implement configurable menu
This commit is contained in:
parent
3e2bdaebc6
commit
b08663fb28
@ -44,7 +44,7 @@
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-1 p-6 bg-base-100 overflow-y-auto">
|
||||
<ng-content></ng-content>
|
||||
<ng-content select="[main-content]"></ng-content>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
@ -57,27 +57,7 @@
|
||||
<!-- Sidebar -->
|
||||
<div class="drawer-side">
|
||||
<label for="my-drawer-2" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
|
||||
<!-- Sidebar content here -->
|
||||
<li class="text-lg font-bold p-4">My App</li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Dashboard</a></li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Analytics</a></li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Reports</a></li>
|
||||
</ul>
|
||||
<ng-content select="[menu-content]"></ng-content>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
<rs-daisy-admin-layout-rs1 (clickEvent)="logout()" [loggedIn]="loggedIn()">
|
||||
<router-outlet />
|
||||
|
||||
<app-menu
|
||||
[title]="menuTitle"
|
||||
[menuItems]="menuConfig"
|
||||
[userRoles]="currentUserRoles"
|
||||
menu-content>
|
||||
</app-menu>
|
||||
<router-outlet main-content />
|
||||
</rs-daisy-admin-layout-rs1>
|
||||
|
||||
@ -1,23 +1,36 @@
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { MainMenu } from './components/main-menu/main-menu';
|
||||
import { AuthService } from './auth/auth.service';
|
||||
import { AdminLayout } from './layout/admin-layout/admin-layout';
|
||||
import { finalize } from 'rxjs/operators';
|
||||
import {Button} from '@rschneider/ng-daisyui';
|
||||
import { AdminLayoutRs1 } from '../../projects/rschneider/ng-daisyui/src/lib/layout';
|
||||
import { Menu, MenuItem } from './components/menu/menu';
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
||||
imports: [RouterOutlet, MainMenu, Button, AdminLayoutRs1],
|
||||
imports: [RouterOutlet, AdminLayoutRs1, Menu],
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css',
|
||||
})
|
||||
export class App {
|
||||
protected readonly title = signal('admin');
|
||||
protected menuConfig: MenuItem[] = [
|
||||
|
||||
];
|
||||
protected currentUserRoles: string[] = ['admin'];
|
||||
protected menuTitle: string | undefined = 'Menü';
|
||||
|
||||
|
||||
constructor(private authService: AuthService, private router: Router) {}
|
||||
constructor(private authService: AuthService, private router: Router) {
|
||||
this.menuConfig = [
|
||||
{
|
||||
menuText: 'Esemény típusok',
|
||||
targetUrl: '/event-type/table',
|
||||
svgIcon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.429 9.75 2.25 12l4.179 2.25m0-4.5 5.571 3 5.571-3m-11.142 0L2.25 7.5 12 2.25l9.75 5.25-4.179 2.25m0 0L21.75 12l-4.179 2.25m0 0 4.179 2.25L12 21.75 2.25 16.5l4.179-2.25m11.142 0-5.571 3-5.571-3" />
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
// With the interceptor fixed, this is now the correct and robust way.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { Component, inject, input, OnInit } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
export interface ActionDefinition<T> {
|
||||
action: string;
|
||||
@ -10,7 +10,6 @@ export interface ActionDefinition<T> {
|
||||
@Component({
|
||||
selector: 'app-generic-action-column',
|
||||
imports: [
|
||||
RouterLink,
|
||||
],
|
||||
templateUrl: './generic-action-column.html',
|
||||
styleUrl: './generic-action-column.css',
|
||||
|
||||
@ -84,5 +84,9 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="text-center p-8">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<p>main-menu works!</p>
|
||||
@ -1,11 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-main-menu',
|
||||
imports: [],
|
||||
templateUrl: './main-menu.html',
|
||||
styleUrl: './main-menu.css',
|
||||
})
|
||||
export class MainMenu {
|
||||
|
||||
}
|
||||
52
admin/src/app/components/menu/menu.html
Normal file
52
admin/src/app/components/menu/menu.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!--
|
||||
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
|
||||
<li class="text-lg font-bold p-4">My App</li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
Dashboard</a></li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Analytics</a></li>
|
||||
<li><a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
Reports</a></li>
|
||||
</ul>
|
||||
-->
|
||||
<ul class="menu p-4 w-80 min-h-full bg-base-300 text-base-content">
|
||||
@if(title()){
|
||||
<li class="text-lg font-bold p-4">{{ title() }}</li>
|
||||
}
|
||||
@for (item of menuItems(); track item.menuText) {
|
||||
|
||||
@if (isItemVisible(item)) {
|
||||
<li [ngClass]="getItemStyleClass(item)">
|
||||
|
||||
@if (item.children && item.children.length > 0) {
|
||||
<details [open]="isSubMenuActive(item)">
|
||||
<summary>
|
||||
<!-- Using [innerHTML] for SVG. Be cautious with non-trusted sources. -->
|
||||
<span [innerHTML]="item.svgIcon | safeHtml"></span>
|
||||
{{ item.menuText }}
|
||||
</summary>
|
||||
<!-- Recursive call to the menu component for nested items -->
|
||||
<app-menu [menuItems]="item.children" [userRoles]="userRoles()"></app-menu>
|
||||
</details>
|
||||
} @else {
|
||||
<a (click)="handleItemClick($event, item)" [class.menu-active]="isItemActive(item)">
|
||||
<span [innerHTML]="item.svgIcon | safeHtml"></span>
|
||||
{{ item.menuText }}
|
||||
</a>
|
||||
}
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
@ -1,18 +1,18 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MainMenu } from './main-menu';
|
||||
import { Menu } from './menu';
|
||||
|
||||
describe('MainMenu', () => {
|
||||
let component: MainMenu;
|
||||
let fixture: ComponentFixture<MainMenu>;
|
||||
describe('Menu', () => {
|
||||
let component: Menu;
|
||||
let fixture: ComponentFixture<Menu>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MainMenu]
|
||||
imports: [Menu]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MainMenu);
|
||||
fixture = TestBed.createComponent(Menu);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
82
admin/src/app/components/menu/menu.ts
Normal file
82
admin/src/app/components/menu/menu.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { SafeHtmlPipe } from '../../pipes/safe-html-pipe';
|
||||
|
||||
export interface MenuItem {
|
||||
id?: string;
|
||||
menuText: string;
|
||||
targetUrl?: string;
|
||||
onClick?: (event: MouseEvent, item: MenuItem) => void;
|
||||
roles?: string[];
|
||||
children?: MenuItem[];
|
||||
getStyleClass?: (item: MenuItem) => string;
|
||||
isActive?: (item: MenuItem, router: Router) => boolean;
|
||||
svgIcon?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
imports: [CommonModule, RouterModule, SafeHtmlPipe],
|
||||
templateUrl: './menu.html',
|
||||
styleUrl: './menu.css',
|
||||
})
|
||||
export class Menu {
|
||||
title = input<string>();
|
||||
// Use the input() function instead of the @Input() decorator.
|
||||
// input.required makes sure that the menuItems array is always passed.
|
||||
menuItems = input.required<MenuItem[]>();
|
||||
|
||||
// You can also provide a default value with input().
|
||||
// In a real app, this would likely come from an authentication service.
|
||||
userRoles = input<string[]>(['admin', 'user']);
|
||||
|
||||
// Use inject() for dependency injection instead of the constructor.
|
||||
private router = inject(Router);
|
||||
|
||||
isItemVisible(item: MenuItem): boolean {
|
||||
if (!item.roles || item.roles.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// Access the value of the signal by calling it as a function: this.userRoles()
|
||||
return item.roles.some(role => this.userRoles().includes(role));
|
||||
}
|
||||
|
||||
handleItemClick(event: MouseEvent, item: MenuItem): void {
|
||||
if (item.onClick) {
|
||||
item.onClick(event, item);
|
||||
} else if (item.targetUrl) {
|
||||
this.router.navigate([item.targetUrl]);
|
||||
}
|
||||
}
|
||||
|
||||
getItemStyleClass(item: MenuItem): string {
|
||||
return item.getStyleClass ? item.getStyleClass(item) : '';
|
||||
}
|
||||
|
||||
isItemActive(item: MenuItem): boolean {
|
||||
if (item.isActive) {
|
||||
return item.isActive(item, this.router);
|
||||
}
|
||||
if (item.targetUrl) {
|
||||
// router.isActive is a boolean check, perfect for this use case.
|
||||
return this.router.isActive(item.targetUrl, {
|
||||
paths: 'exact',
|
||||
queryParams: 'exact',
|
||||
fragment: 'ignored',
|
||||
matrixParams: 'ignored'
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Helper function to determine if a dropdown should be open by default
|
||||
isSubMenuActive(item: MenuItem): boolean {
|
||||
if (!item.children) {
|
||||
return false;
|
||||
}
|
||||
// Recursively check if any child or grandchild is active
|
||||
return item.children.some(child => this.isItemActive(child) || this.isSubMenuActive(child));
|
||||
}
|
||||
}
|
||||
8
admin/src/app/pipes/safe-html-pipe.spec.ts
Normal file
8
admin/src/app/pipes/safe-html-pipe.spec.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { SafeHtmlPipe } from './safe-html-pipe';
|
||||
|
||||
describe('SafeHtmlPipe', () => {
|
||||
it('create an instance', () => {
|
||||
const pipe = new SafeHtmlPipe();
|
||||
expect(pipe).toBeTruthy();
|
||||
});
|
||||
});
|
||||
16
admin/src/app/pipes/safe-html-pipe.ts
Normal file
16
admin/src/app/pipes/safe-html-pipe.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { inject, Pipe, PipeTransform } from '@angular/core';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
|
||||
@Pipe({
|
||||
name: 'safeHtml'
|
||||
})
|
||||
export class SafeHtmlPipe implements PipeTransform {
|
||||
|
||||
private sanitized = inject(DomSanitizer);
|
||||
|
||||
transform(value: string | undefined): SafeHtml {
|
||||
if (!value) return '';
|
||||
return this.sanitized.bypassSecurityTrustHtml(value);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional } from 'class-validator';
|
||||
|
||||
@Entity({ name: 'event_type' })
|
||||
export class EventType {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ -20,4 +23,4 @@ export class EventType {
|
||||
@IsString()
|
||||
color: string | null;
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
// dvbooking-cli/src/templates/nestjs/entity.ts.tpl
|
||||
|
||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
||||
import { IsString, IsNumber, IsBoolean, IsDate, IsOptional } from 'class-validator';
|
||||
|
||||
@Entity({ name: 'products' })
|
||||
export class Product {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@ -20,4 +23,4 @@ export class Product {
|
||||
@IsBoolean()
|
||||
is_available: boolean | null = true;
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,29 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { EventTypesService } from './event-type.service';
|
||||
import { CreateEventTypeDto } from './dto/create-event-type.dto';
|
||||
import { UpdateEventTypeDto } from './dto/update-event-type.dto';
|
||||
import { QueryEventTypeDto } from './dto/query-event-type.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { Role } from '../auth/role.enum';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
|
||||
@Controller('event-type')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(Role.Admin)
|
||||
export class EventTypesController {
|
||||
constructor(private readonly eventTypesService: EventTypesService) {}
|
||||
|
||||
@ -33,7 +52,10 @@ export class EventTypesController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() updateEventTypeDto: UpdateEventTypeDto) {
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateEventTypeDto: UpdateEventTypeDto,
|
||||
) {
|
||||
return this.eventTypesService.update(id, updateEventTypeDto);
|
||||
}
|
||||
|
||||
|
||||
@ -1,10 +1,29 @@
|
||||
import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
ParseIntPipe,
|
||||
DefaultValuePipe,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ProductsService } from './products.service';
|
||||
import { CreateProductDto } from './dto/create-product.dto';
|
||||
import { UpdateProductDto } from './dto/update-product.dto';
|
||||
import { QueryProductDto } from './dto/query-product.dto';
|
||||
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { Role } from '../auth/role.enum';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
|
||||
@Controller('products')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard)
|
||||
@Roles(Role.Admin)
|
||||
export class ProductsController {
|
||||
constructor(private readonly productsService: ProductsService) {}
|
||||
|
||||
@ -33,7 +52,10 @@ export class ProductsController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id', ParseIntPipe) id: number, @Body() updateProductDto: UpdateProductDto) {
|
||||
update(
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Body() updateProductDto: UpdateProductDto,
|
||||
) {
|
||||
return this.productsService.update(id, updateProductDto);
|
||||
}
|
||||
|
||||
@ -41,4 +63,4 @@ export class ProductsController {
|
||||
remove(@Param('id', ParseIntPipe) id: number) {
|
||||
return this.productsService.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user