From b08663fb28980537ae52fe8917365f297ba8b6fc Mon Sep 17 00:00:00 2001 From: Roland Schneider Date: Thu, 20 Nov 2025 09:30:35 +0100 Subject: [PATCH] implement configurable menu --- .../admin-layout-rs1/admin-layout-rs1.html | 26 +----- admin/src/app/app.html | 9 +- admin/src/app/app.ts | 25 ++++-- .../generic-action-column.ts | 3 +- .../generic-table/generic-table.html | 4 + .../app/components/main-menu/main-menu.html | 1 - .../src/app/components/main-menu/main-menu.ts | 11 --- .../main-menu.css => menu/menu.css} | 0 admin/src/app/components/menu/menu.html | 52 ++++++++++++ .../main-menu.spec.ts => menu/menu.spec.ts} | 12 +-- admin/src/app/components/menu/menu.ts | 82 +++++++++++++++++++ admin/src/app/pipes/safe-html-pipe.spec.ts | 8 ++ admin/src/app/pipes/safe-html-pipe.ts | 16 ++++ server/src/entity/event-type.entity.ts | 5 +- server/src/entity/product.entity.ts | 5 +- .../src/event-type/event-type.controller.ts | 26 +++++- server/src/product/products.controller.ts | 28 ++++++- 17 files changed, 256 insertions(+), 57 deletions(-) delete mode 100644 admin/src/app/components/main-menu/main-menu.html delete mode 100644 admin/src/app/components/main-menu/main-menu.ts rename admin/src/app/components/{main-menu/main-menu.css => menu/menu.css} (100%) create mode 100644 admin/src/app/components/menu/menu.html rename admin/src/app/components/{main-menu/main-menu.spec.ts => menu/menu.spec.ts} (60%) create mode 100644 admin/src/app/components/menu/menu.ts create mode 100644 admin/src/app/pipes/safe-html-pipe.spec.ts create mode 100644 admin/src/app/pipes/safe-html-pipe.ts diff --git a/admin/projects/rschneider/ng-daisyui/src/lib/layout/admin-layout-rs1/admin-layout-rs1.html b/admin/projects/rschneider/ng-daisyui/src/lib/layout/admin-layout-rs1/admin-layout-rs1.html index 48c6561..7a1bb80 100644 --- a/admin/projects/rschneider/ng-daisyui/src/lib/layout/admin-layout-rs1/admin-layout-rs1.html +++ b/admin/projects/rschneider/ng-daisyui/src/lib/layout/admin-layout-rs1/admin-layout-rs1.html @@ -44,7 +44,7 @@
- +
@@ -57,27 +57,7 @@
- +
+ diff --git a/admin/src/app/app.html b/admin/src/app/app.html index ffc3a9f..7d2fa33 100644 --- a/admin/src/app/app.html +++ b/admin/src/app/app.html @@ -1,3 +1,10 @@ - + + + + diff --git a/admin/src/app/app.ts b/admin/src/app/app.ts index b66911f..28b8898 100644 --- a/admin/src/app/app.ts +++ b/admin/src/app/app.ts @@ -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: ` + + +` + } + ] + } logout(): void { // With the interceptor fixed, this is now the correct and robust way. diff --git a/admin/src/app/components/generic-action-column/generic-action-column.ts b/admin/src/app/components/generic-action-column/generic-action-column.ts index f73b58c..839be1e 100644 --- a/admin/src/app/components/generic-action-column/generic-action-column.ts +++ b/admin/src/app/components/generic-action-column/generic-action-column.ts @@ -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 { action: string; @@ -10,7 +10,6 @@ export interface ActionDefinition { @Component({ selector: 'app-generic-action-column', imports: [ - RouterLink, ], templateUrl: './generic-action-column.html', styleUrl: './generic-action-column.css', diff --git a/admin/src/app/components/generic-table/generic-table.html b/admin/src/app/components/generic-table/generic-table.html index efba68b..75c39e8 100644 --- a/admin/src/app/components/generic-table/generic-table.html +++ b/admin/src/app/components/generic-table/generic-table.html @@ -84,5 +84,9 @@ } + } @else { +
+ +
} diff --git a/admin/src/app/components/main-menu/main-menu.html b/admin/src/app/components/main-menu/main-menu.html deleted file mode 100644 index 28bd6d6..0000000 --- a/admin/src/app/components/main-menu/main-menu.html +++ /dev/null @@ -1 +0,0 @@ -

main-menu works!

diff --git a/admin/src/app/components/main-menu/main-menu.ts b/admin/src/app/components/main-menu/main-menu.ts deleted file mode 100644 index abdd861..0000000 --- a/admin/src/app/components/main-menu/main-menu.ts +++ /dev/null @@ -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 { - -} diff --git a/admin/src/app/components/main-menu/main-menu.css b/admin/src/app/components/menu/menu.css similarity index 100% rename from admin/src/app/components/main-menu/main-menu.css rename to admin/src/app/components/menu/menu.css diff --git a/admin/src/app/components/menu/menu.html b/admin/src/app/components/menu/menu.html new file mode 100644 index 0000000..a35351b --- /dev/null +++ b/admin/src/app/components/menu/menu.html @@ -0,0 +1,52 @@ + + diff --git a/admin/src/app/components/main-menu/main-menu.spec.ts b/admin/src/app/components/menu/menu.spec.ts similarity index 60% rename from admin/src/app/components/main-menu/main-menu.spec.ts rename to admin/src/app/components/menu/menu.spec.ts index 86c5590..eafdb54 100644 --- a/admin/src/app/components/main-menu/main-menu.spec.ts +++ b/admin/src/app/components/menu/menu.spec.ts @@ -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; +describe('Menu', () => { + let component: Menu; + let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MainMenu] + imports: [Menu] }) .compileComponents(); - fixture = TestBed.createComponent(MainMenu); + fixture = TestBed.createComponent(Menu); component = fixture.componentInstance; fixture.detectChanges(); }); diff --git a/admin/src/app/components/menu/menu.ts b/admin/src/app/components/menu/menu.ts new file mode 100644 index 0000000..aedd838 --- /dev/null +++ b/admin/src/app/components/menu/menu.ts @@ -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(); +// Use the input() function instead of the @Input() decorator. + // input.required makes sure that the menuItems array is always passed. + menuItems = input.required(); + + // You can also provide a default value with input(). + // In a real app, this would likely come from an authentication service. + userRoles = input(['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)); + } +} diff --git a/admin/src/app/pipes/safe-html-pipe.spec.ts b/admin/src/app/pipes/safe-html-pipe.spec.ts new file mode 100644 index 0000000..2428d10 --- /dev/null +++ b/admin/src/app/pipes/safe-html-pipe.spec.ts @@ -0,0 +1,8 @@ +import { SafeHtmlPipe } from './safe-html-pipe'; + +describe('SafeHtmlPipe', () => { + it('create an instance', () => { + const pipe = new SafeHtmlPipe(); + expect(pipe).toBeTruthy(); + }); +}); diff --git a/admin/src/app/pipes/safe-html-pipe.ts b/admin/src/app/pipes/safe-html-pipe.ts new file mode 100644 index 0000000..de157d6 --- /dev/null +++ b/admin/src/app/pipes/safe-html-pipe.ts @@ -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); + } + +} diff --git a/server/src/entity/event-type.entity.ts b/server/src/entity/event-type.entity.ts index 87c4545..6e2e2e5 100644 --- a/server/src/entity/event-type.entity.ts +++ b/server/src/entity/event-type.entity.ts @@ -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; -} +} \ No newline at end of file diff --git a/server/src/entity/product.entity.ts b/server/src/entity/product.entity.ts index 6037c72..eb5870c 100644 --- a/server/src/entity/product.entity.ts +++ b/server/src/entity/product.entity.ts @@ -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; -} +} \ No newline at end of file diff --git a/server/src/event-type/event-type.controller.ts b/server/src/event-type/event-type.controller.ts index 7637023..4e9c80a 100644 --- a/server/src/event-type/event-type.controller.ts +++ b/server/src/event-type/event-type.controller.ts @@ -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); } diff --git a/server/src/product/products.controller.ts b/server/src/product/products.controller.ts index f9a06b9..d328afb 100644 --- a/server/src/product/products.controller.ts +++ b/server/src/product/products.controller.ts @@ -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); } -} \ No newline at end of file +}