Admin sidebar nav, topbar icon buttons (menu, search, quick-publish, tenant selector, notifications), and the product-form translations disclosure icon now render via app-icon instead of PrimeIcons classes. AdminNavLink/AdminNavAction.icon retyped to AppIconName. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, HostListener, computed, inject, signal, viewChild } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
|
import { filter } from 'rxjs/operators';
|
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
|
import { TranslateService } from '../../../i18n/translate.service';
|
|
import { LanguageService } from '../../../services/language.service';
|
|
import { AdminAuthService } from '../../../core/admin-auth/admin-auth.service';
|
|
import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model';
|
|
import { IconComponent } from '../../../shared/ui/icon/icon.component';
|
|
|
|
@Component({
|
|
selector: 'app-admin-layout',
|
|
standalone: true,
|
|
imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive, TranslatePipe, IconComponent],
|
|
templateUrl: './admin-layout.component.html',
|
|
styleUrl: './admin-layout.component.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class AdminLayoutComponent {
|
|
private readonly router = inject(Router);
|
|
private readonly route = inject(ActivatedRoute);
|
|
private readonly destroyRef = inject(DestroyRef);
|
|
private readonly translate = inject(TranslateService);
|
|
private readonly languageService = inject(LanguageService);
|
|
private readonly adminAuth = inject(AdminAuthService);
|
|
|
|
private readonly drawerEl = viewChild<ElementRef<HTMLElement>>('drawer');
|
|
private readonly menuToggleEl = viewChild<ElementRef<HTMLElement>>('menuToggle');
|
|
|
|
readonly navPrimary: AdminNavEntry[] = ADMIN_NAV_PRIMARY;
|
|
readonly navBottom: AdminNavEntry[] = ADMIN_NAV_BOTTOM;
|
|
|
|
readonly mobileDrawerOpen = signal(false);
|
|
readonly notificationsOpen = signal(false);
|
|
|
|
readonly currentLang = this.languageService.currentLanguage;
|
|
readonly displayName = this.adminAuth.displayName;
|
|
|
|
readonly initials = computed(() => {
|
|
const name = this.displayName();
|
|
if (!name) {
|
|
return '?';
|
|
}
|
|
return name
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map(part => part[0]?.toUpperCase())
|
|
.join('');
|
|
});
|
|
|
|
private readonly breadcrumbSignal = signal<AdminBreadcrumbEntry[]>([]);
|
|
private readonly titleKeySignal = signal<string>('adminShell.pages.dashboard.title');
|
|
private readonly descriptionKeySignal = signal<string>('adminShell.pages.dashboard.description');
|
|
|
|
readonly breadcrumb = this.breadcrumbSignal.asReadonly();
|
|
readonly pageTitleKey = this.titleKeySignal.asReadonly();
|
|
readonly pageDescriptionKey = this.descriptionKeySignal.asReadonly();
|
|
|
|
constructor() {
|
|
this.readRouteData();
|
|
this.router.events
|
|
.pipe(
|
|
filter(event => event instanceof NavigationEnd),
|
|
takeUntilDestroyed(this.destroyRef),
|
|
)
|
|
.subscribe(() => {
|
|
this.readRouteData();
|
|
this.closeMobileDrawer();
|
|
this.notificationsOpen.set(false);
|
|
});
|
|
}
|
|
|
|
@HostListener('document:keydown.escape')
|
|
onEscape(): void {
|
|
if (this.mobileDrawerOpen()) {
|
|
this.closeMobileDrawer();
|
|
return;
|
|
}
|
|
if (this.notificationsOpen()) {
|
|
this.notificationsOpen.set(false);
|
|
}
|
|
}
|
|
|
|
toggleMobileDrawer(): void {
|
|
this.mobileDrawerOpen.update(open => !open);
|
|
if (this.mobileDrawerOpen()) {
|
|
queueMicrotask(() => {
|
|
const drawer = this.drawerEl()?.nativeElement;
|
|
drawer?.querySelector<HTMLElement>('a, button')?.focus();
|
|
});
|
|
}
|
|
}
|
|
|
|
closeMobileDrawer(): void {
|
|
if (!this.mobileDrawerOpen()) {
|
|
return;
|
|
}
|
|
this.mobileDrawerOpen.set(false);
|
|
this.menuToggleEl()?.nativeElement.focus();
|
|
}
|
|
|
|
toggleNotifications(): void {
|
|
this.notificationsOpen.update(open => !open);
|
|
}
|
|
|
|
adminLinkFor(entry: Extract<AdminNavEntry, { type: 'link' }>): string[] {
|
|
const lang = this.currentLang();
|
|
if (entry.absolutePath) {
|
|
return ['/', lang, ...entry.absolutePath];
|
|
}
|
|
return ['/', lang, 'backoffice', ...(entry.path ?? [])];
|
|
}
|
|
|
|
breadcrumbLink(entry: AdminBreadcrumbEntry): string[] {
|
|
return ['/', this.currentLang(), 'backoffice', ...(entry.path ?? [])];
|
|
}
|
|
|
|
logout(): void {
|
|
this.adminAuth.logout();
|
|
}
|
|
|
|
cardTitle(key: string): string {
|
|
return this.translate.t(key);
|
|
}
|
|
|
|
private readRouteData(): void {
|
|
let leaf = this.route;
|
|
while (leaf.firstChild) {
|
|
leaf = leaf.firstChild;
|
|
}
|
|
// Route metadata is optional: during the initial navigation (or for routes
|
|
// registered without `data`) the leaf snapshot may not exist yet. Fall back
|
|
// to dashboard defaults instead of crashing the whole admin shell.
|
|
const data = (leaf.snapshot?.data ?? {}) as {
|
|
titleKey?: string;
|
|
descriptionKey?: string;
|
|
breadcrumb?: AdminBreadcrumbEntry[];
|
|
};
|
|
this.titleKeySignal.set(data.titleKey ?? 'adminShell.pages.dashboard.title');
|
|
this.descriptionKeySignal.set(data.descriptionKey ?? 'adminShell.pages.dashboard.description');
|
|
this.breadcrumbSignal.set(data.breadcrumb ?? []);
|
|
}
|
|
}
|