2024-10-06 23:56:03 +02:00
|
|
|
import {
|
|
|
|
|
customElement,
|
|
|
|
|
DeesElement,
|
|
|
|
|
property,
|
|
|
|
|
html,
|
|
|
|
|
cssManager,
|
|
|
|
|
unsafeCSS,
|
|
|
|
|
css,
|
2025-12-01 09:44:37 +00:00
|
|
|
state,
|
2024-10-06 23:56:03 +02:00
|
|
|
type TemplateResult,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
|
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
|
import * as states from '../../states/accountstate.js';
|
2024-10-07 10:26:21 +02:00
|
|
|
import { IdpState } from '../../states/idp.state.js';
|
2025-12-01 04:08:17 +00:00
|
|
|
import { accountDesignTokens } from './sharedstyles.js';
|
2025-12-01 20:03:34 +00:00
|
|
|
import { CreateOrgModal } from './create-org-modal.js';
|
|
|
|
|
import { OrgSelectModal } from './org-select-modal.js';
|
2024-10-07 10:26:21 +02:00
|
|
|
|
|
|
|
|
import { commitinfo } from '../../../dist_ts/00_commitinfo_data.js';
|
2024-10-06 23:56:03 +02:00
|
|
|
|
|
|
|
|
declare global {
|
|
|
|
|
interface HTMLElementTagNameMap {
|
|
|
|
|
'lele-accountnavigation': LeleAccountNavigation;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@customElement('lele-accountnavigation')
|
|
|
|
|
export class LeleAccountNavigation extends DeesElement {
|
2025-12-01 09:44:37 +00:00
|
|
|
@state()
|
|
|
|
|
accessor isGlobalAdmin: boolean = false;
|
|
|
|
|
|
2025-12-01 20:03:34 +00:00
|
|
|
@state()
|
|
|
|
|
accessor currentPath: string = window.location.pathname;
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
constructor() {
|
|
|
|
|
super();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 20:03:34 +00:00
|
|
|
private async navigateTo(path: string) {
|
|
|
|
|
const subrouter = await this.getAccountRouter();
|
|
|
|
|
subrouter.pushUrl(path);
|
|
|
|
|
// Update state after navigation to trigger re-render
|
|
|
|
|
this.currentPath = window.location.pathname;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async navigateToOrgPage(page: string) {
|
|
|
|
|
const currentState = states.accountState.getState();
|
|
|
|
|
if (currentState.selectedOrg) {
|
|
|
|
|
const path = page ? `/org/${currentState.selectedOrg.data.slug}/${page}` : `/org/${currentState.selectedOrg.data.slug}`;
|
|
|
|
|
await this.navigateTo(path);
|
|
|
|
|
} else {
|
|
|
|
|
const targetPath = page ? `/org/:orgName/${page}` : '/org/:orgName';
|
|
|
|
|
const description = page ? `Choose an organization to view its ${page}.` : 'Choose an organization to view its overview.';
|
|
|
|
|
const result = await OrgSelectModal.show({
|
|
|
|
|
targetPath,
|
|
|
|
|
title: 'Select Organization',
|
|
|
|
|
description,
|
|
|
|
|
});
|
|
|
|
|
if (result) {
|
|
|
|
|
await this.navigateTo(result.path.replace('/account', ''));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
public static styles = [
|
|
|
|
|
cssManager.defaultStyles,
|
2025-12-01 04:08:17 +00:00
|
|
|
accountDesignTokens,
|
2024-10-06 23:56:03 +02:00
|
|
|
css`
|
|
|
|
|
:host {
|
2025-12-01 04:08:17 +00:00
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
background: var(--card);
|
|
|
|
|
border-right: 1px solid var(--border);
|
|
|
|
|
height: 100%;
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
|
|
|
|
:host([hidden]) {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 04:08:17 +00:00
|
|
|
.logoArea {
|
|
|
|
|
padding: 20px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border);
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-07 15:14:44 +02:00
|
|
|
.logo {
|
2025-12-01 04:08:17 +00:00
|
|
|
font-family: 'Cal Sans', 'Geist Sans', sans-serif;
|
|
|
|
|
letter-spacing: -0.02em;
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--foreground);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: opacity 0.15s ease;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2024-10-07 15:14:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logo:hover {
|
2025-12-01 04:08:17 +00:00
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.logo dees-icon {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navContent {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding-bottom: 16px;
|
2024-10-07 15:14:44 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-07 10:26:21 +02:00
|
|
|
.commitinfo {
|
2025-12-01 04:08:17 +00:00
|
|
|
flex-shrink: 0;
|
2024-10-07 10:26:21 +02:00
|
|
|
text-align: center;
|
2025-12-01 04:08:17 +00:00
|
|
|
font-family: 'Geist Mono', monospace;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-top: 1px solid var(--border);
|
|
|
|
|
color: var(--muted-foreground);
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
background: var(--card);
|
2024-10-07 10:26:21 +02:00
|
|
|
}
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
.navigationGroupLabel {
|
2025-12-01 04:08:17 +00:00
|
|
|
font-size: 10px;
|
|
|
|
|
font-weight: 600;
|
2024-10-06 23:56:03 +02:00
|
|
|
text-transform: uppercase;
|
2025-12-01 04:08:17 +00:00
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
color: var(--muted-foreground);
|
|
|
|
|
padding: 20px 16px 8px;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationGroupLabel:first-of-type {
|
|
|
|
|
padding-top: 16px;
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationOption {
|
2025-12-01 04:08:17 +00:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
margin: 2px 8px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-size: 13px;
|
2024-10-06 23:56:03 +02:00
|
|
|
font-weight: 500;
|
2025-12-01 04:08:17 +00:00
|
|
|
color: var(--muted-foreground);
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
cursor: pointer;
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationOption:hover {
|
2025-12-01 04:08:17 +00:00
|
|
|
background: var(--muted);
|
|
|
|
|
color: var(--foreground);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationOption dees-icon {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationOption:hover dees-icon {
|
|
|
|
|
opacity: 1;
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
2025-12-01 04:08:17 +00:00
|
|
|
|
2025-12-01 20:03:34 +00:00
|
|
|
.navigationOption.active {
|
|
|
|
|
background: var(--muted);
|
|
|
|
|
color: var(--foreground);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.navigationOption.active dees-icon {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 04:08:17 +00:00
|
|
|
.divider {
|
|
|
|
|
height: 1px;
|
|
|
|
|
background: var(--border);
|
|
|
|
|
margin: 8px 16px;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
dees-input-dropdown {
|
2025-12-01 04:08:17 +00:00
|
|
|
margin: 8px;
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
|
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
2024-10-07 15:14:44 +02:00
|
|
|
public async getAccountRouter() {
|
|
|
|
|
const host = (this.getRootNode() as any).host;
|
|
|
|
|
return (host as any).subrouter;
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
public render(): TemplateResult {
|
|
|
|
|
return html`
|
2025-12-01 04:08:17 +00:00
|
|
|
<div class="logoArea">
|
|
|
|
|
<div class="logo">
|
|
|
|
|
<dees-icon .icon=${'lucide:fingerprint'}></dees-icon>
|
|
|
|
|
idp.global
|
|
|
|
|
</div>
|
2024-10-07 10:26:21 +02:00
|
|
|
</div>
|
2025-12-01 04:08:17 +00:00
|
|
|
|
|
|
|
|
<div class="navContent">
|
|
|
|
|
<div class="navigationGroupLabel">Account</div>
|
|
|
|
|
<div
|
2025-12-01 20:03:34 +00:00
|
|
|
class="navigationOption ${this.isActive('') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateTo('')}
|
2025-12-01 04:08:17 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:home'}></dees-icon>
|
|
|
|
|
Overview
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="navigationOption"
|
|
|
|
|
@click=${async () => {
|
|
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:shield'}></dees-icon>
|
|
|
|
|
Manage Roles
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="navigationOption"
|
|
|
|
|
@click=${async () => {
|
|
|
|
|
const idpState = await IdpState.getSingletonInstance();
|
|
|
|
|
idpState.domtools.router.pushUrl('/logout');
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:power'}></dees-icon>
|
|
|
|
|
Log Out
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="divider"></div>
|
|
|
|
|
|
|
|
|
|
<div class="navigationGroupLabel">Organization</div>
|
|
|
|
|
<dees-input-dropdown
|
|
|
|
|
.label=${'Select organization'}
|
2025-12-01 18:56:16 +00:00
|
|
|
@selectedOption=${async (eventArg: CustomEvent) => {
|
|
|
|
|
// Handle "Create new..." option
|
|
|
|
|
if (eventArg.detail.key === '__create_new__') {
|
2025-12-01 20:03:34 +00:00
|
|
|
const org = await CreateOrgModal.show();
|
|
|
|
|
if (org) {
|
|
|
|
|
await this.navigateTo(`/org/${org.data.slug}/billing`);
|
|
|
|
|
}
|
2025-12-01 18:56:16 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2025-12-01 04:08:17 +00:00
|
|
|
const currentState = states.accountState.getState();
|
2025-12-01 18:56:16 +00:00
|
|
|
const newOrg = currentState.organizations.find((org) => org.data.slug === eventArg.detail.payload);
|
|
|
|
|
states.accountState.dispatchAction(states.setSelectedOrg, newOrg);
|
|
|
|
|
|
|
|
|
|
// Auto-navigate to new org's current page type (reactivity)
|
|
|
|
|
const currentPath = window.location.pathname;
|
|
|
|
|
if (currentPath.includes('/org/') && newOrg) {
|
|
|
|
|
// Extract the page type (apps, billing, etc.) and navigate to new org
|
|
|
|
|
const pathParts = currentPath.split('/');
|
|
|
|
|
const pageType = pathParts[5]; // /account/org/:orgName/:pageType
|
|
|
|
|
if (pageType) {
|
2025-12-01 20:03:34 +00:00
|
|
|
await this.navigateTo(`/org/${newOrg.data.slug}/${pageType}`);
|
2025-12-01 18:56:16 +00:00
|
|
|
} else {
|
2025-12-01 20:03:34 +00:00
|
|
|
await this.navigateTo(`/org/${newOrg.data.slug}`);
|
2025-12-01 18:56:16 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-01 04:08:17 +00:00
|
|
|
}}
|
|
|
|
|
></dees-input-dropdown>
|
|
|
|
|
|
2025-12-01 18:56:16 +00:00
|
|
|
<div
|
2025-12-01 20:03:34 +00:00
|
|
|
class="navigationOption ${this.isActive('org-overview') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateToOrgPage('')}
|
2025-12-01 18:56:16 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:home'}></dees-icon>
|
|
|
|
|
Overview
|
|
|
|
|
</div>
|
2025-12-01 04:08:17 +00:00
|
|
|
<div
|
2025-12-01 20:03:34 +00:00
|
|
|
class="navigationOption ${this.isActive('apps') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateToOrgPage('apps')}
|
2025-12-01 04:08:17 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:box'}></dees-icon>
|
|
|
|
|
Apps
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
2025-12-04 17:45:40 +00:00
|
|
|
class="navigationOption ${this.isActive('users') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateToOrgPage('users')}
|
2025-12-01 04:08:17 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:users'}></dees-icon>
|
|
|
|
|
Users
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
|
|
|
|
class="navigationOption"
|
|
|
|
|
@click=${async () => {}}
|
|
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
|
|
|
|
Activity
|
|
|
|
|
</div>
|
|
|
|
|
<div
|
2025-12-01 20:03:34 +00:00
|
|
|
class="navigationOption ${this.isActive('billing') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateToOrgPage('billing')}
|
2025-12-01 04:08:17 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:wallet'}></dees-icon>
|
|
|
|
|
Billing
|
|
|
|
|
</div>
|
2025-12-01 09:44:37 +00:00
|
|
|
|
|
|
|
|
${this.renderAdminLink()}
|
2024-10-07 10:26:21 +02:00
|
|
|
</div>
|
2025-12-01 04:08:17 +00:00
|
|
|
|
|
|
|
|
<div class="commitinfo">v${commitinfo.version}</div>
|
2024-10-06 23:56:03 +02:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 09:44:37 +00:00
|
|
|
private renderAdminLink(): TemplateResult | null {
|
|
|
|
|
if (!this.isGlobalAdmin) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return html`
|
|
|
|
|
<div class="divider"></div>
|
|
|
|
|
<div class="navigationGroupLabel">Platform</div>
|
|
|
|
|
<div
|
2025-12-01 20:03:34 +00:00
|
|
|
class="navigationOption ${this.isActive('admin') ? 'active' : ''}"
|
|
|
|
|
@click=${() => this.navigateTo('/admin')}
|
2025-12-01 09:44:37 +00:00
|
|
|
>
|
|
|
|
|
<dees-icon .icon=${'lucide:shield'}></dees-icon>
|
|
|
|
|
Global Admin
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-01 20:03:34 +00:00
|
|
|
private isActive(page: string): boolean {
|
|
|
|
|
const path = this.currentPath;
|
|
|
|
|
if (page === '') {
|
|
|
|
|
// Account overview - exact match
|
|
|
|
|
return path === '/account' || path === '/account/';
|
|
|
|
|
}
|
|
|
|
|
if (page === 'org-overview') {
|
|
|
|
|
// Org overview - /account/org/:slug without trailing page type
|
|
|
|
|
return /^\/account\/org\/[^\/]+\/?$/.test(path);
|
|
|
|
|
}
|
|
|
|
|
// For other pages, check if the path contains the page segment
|
|
|
|
|
return path.includes(`/${page}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async firstUpdated() {
|
|
|
|
|
// Listen for popstate (browser back/forward)
|
|
|
|
|
window.addEventListener('popstate', () => {
|
|
|
|
|
this.currentPath = window.location.pathname;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Watch for URL changes from external navigation (e.g., modals)
|
|
|
|
|
let lastPath = this.currentPath;
|
|
|
|
|
const checkPath = () => {
|
|
|
|
|
if (window.location.pathname !== lastPath) {
|
|
|
|
|
lastPath = window.location.pathname;
|
|
|
|
|
this.currentPath = lastPath;
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(checkPath);
|
|
|
|
|
};
|
|
|
|
|
requestAnimationFrame(checkPath);
|
|
|
|
|
|
2024-10-06 23:56:03 +02:00
|
|
|
const deesInputDropdown = this.shadowRoot.querySelector('dees-input-dropdown');
|
|
|
|
|
const orgToMenuEntry = (orgArg?: plugins.idpInterfaces.data.IOrganization) => {
|
|
|
|
|
if (!orgArg) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
option: orgArg.data.name,
|
|
|
|
|
key: orgArg.data.slug,
|
|
|
|
|
payload: orgArg.data.slug,
|
2024-10-07 10:26:21 +02:00
|
|
|
};
|
|
|
|
|
};
|
2025-12-01 18:56:16 +00:00
|
|
|
|
|
|
|
|
// "Create new..." option to add at the end
|
|
|
|
|
const createNewOption = {
|
|
|
|
|
option: '+ Create new...',
|
|
|
|
|
key: '__create_new__',
|
|
|
|
|
payload: '__create_new__',
|
|
|
|
|
};
|
|
|
|
|
|
2024-10-07 10:26:21 +02:00
|
|
|
states.accountState
|
|
|
|
|
.select((stateArg) => stateArg.organizations)
|
|
|
|
|
.pipe(
|
|
|
|
|
plugins.deesDomtools.plugins.smartrx.rxjs.ops.map((orgArrayArg) => {
|
2025-12-01 18:56:16 +00:00
|
|
|
const orgEntries = orgArrayArg.map(orgToMenuEntry);
|
|
|
|
|
// Add "Create new..." at the end
|
|
|
|
|
return [...orgEntries, createNewOption];
|
2024-10-07 10:26:21 +02:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.subscribe((menuEntries) => {
|
|
|
|
|
deesInputDropdown.options = menuEntries;
|
|
|
|
|
});
|
|
|
|
|
states.accountState
|
|
|
|
|
.select((stateArg) => stateArg.selectedOrg)
|
|
|
|
|
.pipe(plugins.deesDomtools.plugins.smartrx.rxjs.ops.map(orgToMenuEntry))
|
|
|
|
|
.subscribe((selectedOrgArg) => {
|
|
|
|
|
deesInputDropdown.selectedOption = selectedOrgArg;
|
|
|
|
|
});
|
2025-12-01 09:44:37 +00:00
|
|
|
|
|
|
|
|
// Check if user is global admin
|
|
|
|
|
states.accountState
|
|
|
|
|
.select((stateArg) => stateArg.user)
|
|
|
|
|
.subscribe((user) => {
|
|
|
|
|
this.isGlobalAdmin = user?.data?.isGlobalAdmin ?? false;
|
|
|
|
|
});
|
2024-10-06 23:56:03 +02:00
|
|
|
}
|
|
|
|
|
}
|