add generic table

This commit is contained in:
Roland Schneider 2025-11-19 22:48:18 +01:00
parent 1bfa4dec47
commit ec44849b31
6 changed files with 171 additions and 5 deletions

View File

@ -22,10 +22,6 @@ export class GenericTableSearchForm {
this.filterForm.valueChanges.pipe( this.filterForm.valueChanges.pipe(
debounceTime(300), debounceTime(300),
filter( value => {
console.info(value)
return value.term && value.term.length >= 3;
}),
distinctUntilChanged() distinctUntilChanged()
).subscribe(values => { ).subscribe(values => {
const cleanFilter = Object.fromEntries( const cleanFilter = Object.fromEntries(

View File

@ -1,5 +1,8 @@
<div [ngClass]="config.tableCssClass" [class]="'overflow-x-auto'"> <div [ngClass]="config.tableCssClass" [class]="'overflow-x-auto'">
<app-generic-table-search-form (searchTermChanged)="searchTermChanged($event)" ></app-generic-table-search-form> <app-generic-table-search-form (searchTermChanged)="searchTermChanged($event)" ></app-generic-table-search-form>
<div>
</div>
@if (data$ | async; as getDataResponse) { @if (data$ | async; as getDataResponse) {
<table class="table w-full table-zebra"> <table class="table w-full table-zebra">
<thead> <thead>
@ -51,7 +54,20 @@
</tbody> </tbody>
</table> </table>
@if ((getDataResponse?.data?.meta?.totalPages ?? 1) > 1) { @if ((getDataResponse?.data?.meta?.totalPages ?? 1) > 0) {
<div class="flex justify-end mt-4 items-center me-3 ">
Items: {{getDataResponse.data.data.length}}/{{getDataResponse.data.meta.totalItems}}
Page: {{getDataResponse.data.meta.currentPage}}/{{getDataResponse.data.meta.totalPages}}
PageSize:
<select class="select w-auto" #pageSize (change)="onPageSizeChange(pageSize.value)">
<option>10</option>
<option>20</option>
<option>50</option>
</select>
</div>
}
@if ((getDataResponse?.data?.meta?.totalPages ?? 1) > 1) {
<div class="flex justify-center mt-4"> <div class="flex justify-center mt-4">
<div class="join"> <div class="join">
<button <button

View File

@ -88,4 +88,9 @@ export class GenericTable<T> implements OnInit {
this.config.page$.next(1); this.config.page$.next(1);
this.config.filter$.next(searchTerm); this.config.filter$.next(searchTerm);
} }
protected onPageSizeChange(value: any) {
this.config.page$.next(1);
this.config.limit$.next(+value);
}
} }

View File

@ -0,0 +1,26 @@
// dvbooking-cli/src/templates/angular-generic/data-provider.service.ts.tpl
// Generated by the CLI
import { inject, Injectable } from '@angular/core';
import { DataProvider, GetDataOptions, GetDataResponse } from '../../../../components/generic-table/data-provider.interface';
import { Product } from '../../models/product.model';
import { map, Observable } from 'rxjs';
import { ProductService } from '../../services/product.service';
@Injectable({
providedIn: 'root',
})
export class ProductDataProvider implements DataProvider<Product> {
private productService = inject(ProductService);
getData(options?: GetDataOptions): Observable<GetDataResponse<Product>> {
const {q,page,limit} = options?.params ?? {};
// The generic table's params are compatible with our NestJS Query DTO
return this.productService.search(q ?? '',page,limit, ).pipe(
map((res) => {
// Adapt the paginated response to the GetDataResponse format
return { data: res };
})
);
}
}

View File

@ -0,0 +1,11 @@
<!-- dvbooking-cli/src/templates/angular-generic/table.component.html.tpl -->
<!-- Generated by the CLI -->
<div class="p-4 md:p-8 space-y-6">
<div class="flex justify-between items-center">
<h1 class="text-3xl font-bold">Products (Generic Table)</h1>
<a routerLink="/products/new" class="btn btn-primary">Create New</a>
</div>
<app-generic-table [config]="tableConfig"></app-generic-table>
</div>

View File

@ -0,0 +1,112 @@
// dvbooking-cli/src/templates/angular-generic/table.component.ts.tpl
// Generated by the CLI
import { Component, inject, OnInit } from '@angular/core';
import { Router, RouterModule } from '@angular/router';
import { Product } from '../../models/product.model';
import { ProductDataProvider } from './product-data-provider.service';
import { ColumnDefinition } from '../../../../components/generic-table/column-definition.interface';
import { GenericTable } from '../../../../components/generic-table/generic-table';
import { GenericTableConfig } from '../../../../components/generic-table/generic-table.config';
import {
ActionDefinition,
GenericActionColumn,
} from '../../../../components/generic-action-column/generic-action-column';
import { ProductService } from '../../services/product.service';
import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-product-table',
standalone: true,
imports: [GenericTable, RouterModule],
templateUrl: './product-table.component.html',
})
export class ProductTableComponent implements OnInit {
private refresh$ = new BehaviorSubject<void>(undefined);
private filter$ = new BehaviorSubject<any>({});
private page$ = new BehaviorSubject<number>(1);
private limit$ = new BehaviorSubject<number>(10);
router = inject(Router);
tableConfig!: GenericTableConfig<Product>;
productDataProvider = inject(ProductDataProvider);
productService = inject(ProductService);
ngOnInit(): void {
const actionHandler = (action: ActionDefinition<Product>, item: Product) => {
switch (action.action) {
case 'view':
this.router.navigate(['/products', item?.id]);
break;
case 'edit':
this.router.navigate(['/products', item?.id, 'edit']);
break;
case 'delete':
this.deleteItem(item.id);
break;
}
};
this.tableConfig = {
refresh$: this.refresh$,
filter$: this.filter$,
page$: this.page$,
limit$: this.limit$,
dataProvider: this.productDataProvider,
columns: [
{
attribute: 'name',
headerCell: true,
valueCell: true,
},
{
attribute: 'price',
headerCell: true,
valueCell: true,
},
{
attribute: 'is_available',
headerCell: true,
valueCell: {
value: item => (item as any)?.is_available ? 'yes' : 'no',
},
},
{
attribute: 'actions',
headerCell: { value: 'Actions' },
valueCell: {
component: GenericActionColumn,
componentInputs: item => ({
item: item,
actions: [
{ action: 'view', handler: actionHandler },
{ action: 'edit', handler: actionHandler },
{ action: 'delete', handler: actionHandler },
] as ActionDefinition<Product>[],
}),
},
},
] as ColumnDefinition<Product>[],
tableCssClass: 'product-table-container',
};
}
deleteItem(id: number): void {
if (confirm('Are you sure you want to delete this item?')) {
this.productService.remove(id).subscribe({
next: () => {
console.log(`Item with ID ${id} deleted successfully.`);
this.refresh$.next();
},
// --- THIS IS THE FIX ---
// Explicitly type 'err' to satisfy strict TypeScript rules.
error: (err: any) => {
console.error(`Error deleting item with ID ${id}:`, err);
}
});
}
}
}