add generic table
This commit is contained in:
parent
a6d495545a
commit
0a26f8a28e
@ -7,6 +7,7 @@ import { CrudGeneratorService } from './services/crud-generator.service';
|
|||||||
import { ModuleUpdaterService } from './services/module-updater.service';
|
import { ModuleUpdaterService } from './services/module-updater.service';
|
||||||
import { AngularGeneratorService } from './services/angular-generator.service';
|
import { AngularGeneratorService } from './services/angular-generator.service';
|
||||||
import { TemplateService } from './services/template.service';
|
import { TemplateService } from './services/template.service';
|
||||||
|
import { GenericTableGeneratorService } from './services/generic-table-generator.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
@ -17,7 +18,8 @@ import { TemplateService } from './services/template.service';
|
|||||||
CrudGeneratorService,
|
CrudGeneratorService,
|
||||||
ModuleUpdaterService,
|
ModuleUpdaterService,
|
||||||
AngularGeneratorService,
|
AngularGeneratorService,
|
||||||
TemplateService
|
TemplateService,
|
||||||
|
GenericTableGeneratorService
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { Command, CommandRunner } from 'nest-commander';
|
import { Command, CommandRunner } from 'nest-commander';
|
||||||
import { EntityGeneratorService } from '../services/entity-generator.service';
|
import { EntityGeneratorService } from '../services/entity-generator.service';
|
||||||
import { CrudGeneratorService } from '../services/crud-generator.service';
|
import { CrudGeneratorService } from '../services/crud-generator.service';
|
||||||
import { AngularGeneratorService } from '../services/angular-generator.service'; // <-- 1. Import
|
import { AngularGeneratorService } from '../services/angular-generator.service';
|
||||||
|
|
||||||
@Command({
|
@Command({
|
||||||
name: 'generate',
|
name: 'generate',
|
||||||
@ -52,7 +52,7 @@ export class GenerateCommand extends CommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('\n--- Generating CRUD Module ---');
|
console.log('\n--- Generating CRUD Module ---');
|
||||||
await this.crudGeneratorService.generate(name);
|
await this.crudGeneratorService.generate(name, columns);
|
||||||
console.log('\n✨ All backend files generated successfully! ✨');
|
console.log('\n✨ All backend files generated successfully! ✨');
|
||||||
console.log('\n--- Generating Angular Files ---');
|
console.log('\n--- Generating Angular Files ---');
|
||||||
await this.angularGeneratorService.generate(name,columns);
|
await this.angularGeneratorService.generate(name,columns);
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import * as path from 'path';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { TableColumn } from 'typeorm';
|
import { TableColumn } from 'typeorm';
|
||||||
|
import { GenericTableGeneratorService } from './generic-table-generator.service';
|
||||||
|
|
||||||
// Interface for structured field metadata passed to templates
|
// Interface for structured field metadata passed to templates
|
||||||
interface FieldDefinition {
|
interface FieldDefinition {
|
||||||
@ -31,6 +32,7 @@ export class AngularGeneratorService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly templateService: TemplateService,
|
private readonly templateService: TemplateService,
|
||||||
private readonly moduleUpdaterService: ModuleUpdaterService,
|
private readonly moduleUpdaterService: ModuleUpdaterService,
|
||||||
|
private readonly genericTableGeneratorService: GenericTableGeneratorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public async generate(tableName: string, columns?: TableColumn[]): Promise<void> {
|
public async generate(tableName: string, columns?: TableColumn[]): Promise<void> {
|
||||||
@ -70,6 +72,11 @@ export class AngularGeneratorService {
|
|||||||
await this.generateService(names, featureDir);
|
await this.generateService(names, featureDir);
|
||||||
await this.generateFilterComponent(names, featureDir);
|
await this.generateFilterComponent(names, featureDir);
|
||||||
await this.generateListComponent(names, featureDir);
|
await this.generateListComponent(names, featureDir);
|
||||||
|
|
||||||
|
// 3. New Generic Table View
|
||||||
|
const tableCompPath = await this.generateTableComponent(tableName, columns || [], names, featureDir);
|
||||||
|
|
||||||
|
// 4. Details & Form Views
|
||||||
await this.generateDetailsComponent(names, featureDir);
|
await this.generateDetailsComponent(names, featureDir);
|
||||||
await this.generateFormComponent(names, featureDir);
|
await this.generateFormComponent(names, featureDir);
|
||||||
|
|
||||||
@ -80,16 +87,23 @@ export class AngularGeneratorService {
|
|||||||
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/new`);
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/new`);
|
||||||
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/:id/edit`);
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}FormComponent`, formCompPath, `${names.plural}/:id/edit`);
|
||||||
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}DetailsComponent`, detailsCompPath, `${names.plural}/:id`);
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}DetailsComponent`, detailsCompPath, `${names.plural}/:id`);
|
||||||
|
|
||||||
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}TableComponent`, tableCompPath, `${names.plural}/table`);
|
||||||
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}ListComponent`, listCompPath, names.plural);
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}ListComponent`, listCompPath, names.plural);
|
||||||
|
|
||||||
console.log(`✅ Angular files for "${tableName}" created successfully in: ${featureDir}`);
|
console.log(`✅ Angular files for "${tableName}" created successfully in: ${featureDir}`);
|
||||||
console.log('\n✨ app.routes.ts has been updated with full CRUD routes! ✨');
|
console.log('\n✨ app.routes.ts has been updated with full CRUD routes (List + Table)! ✨');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ An error occurred during Angular generation:`, error.message);
|
console.error(`❌ An error occurred during Angular generation:`, error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async generateTableComponent(tableName: string, columns: TableColumn[], names: NamingConvention, featureDir: string): Promise<string> {
|
||||||
|
// This method now correctly calls the renamed service
|
||||||
|
return this.genericTableGeneratorService.generate(tableName, columns, names, featureDir);
|
||||||
|
}
|
||||||
|
|
||||||
private getFormFieldHtml(field: FieldDefinition, isFilter: boolean = false): string {
|
private getFormFieldHtml(field: FieldDefinition, isFilter: boolean = false): string {
|
||||||
const label = `<label class="label"><span class="label-text">${field.name}</span></label>`;
|
const label = `<label class="label"><span class="label-text">${field.name}</span></label>`;
|
||||||
const placeholder = isFilter ? `placeholder="Filter by ${field.name}"` : '';
|
const placeholder = isFilter ? `placeholder="Filter by ${field.name}"` : '';
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import * as path from 'path';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { promises as fsPromises } from 'fs';
|
import { promises as fsPromises } from 'fs';
|
||||||
import { ModuleUpdaterService } from './module-updater.service';
|
import { ModuleUpdaterService } from './module-updater.service';
|
||||||
|
import { TableColumn } from 'typeorm';
|
||||||
|
|
||||||
interface NamingConvention {
|
interface NamingConvention {
|
||||||
singular: string;
|
singular: string;
|
||||||
plural: string;
|
plural: string;
|
||||||
pascal: string;
|
pascal: string;
|
||||||
camel: string;
|
camel: string;
|
||||||
|
searchableFields
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -18,13 +20,15 @@ export class CrudGeneratorService {
|
|||||||
constructor(private readonly configService: ConfigService, private readonly moduleUpdaterService: ModuleUpdaterService,) {
|
constructor(private readonly configService: ConfigService, private readonly moduleUpdaterService: ModuleUpdaterService,) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async generate(tableName: string): Promise<void> {
|
public async generate(tableName: string, columns?: TableColumn[]): Promise<void> {
|
||||||
console.log(`Generating CRUD module for table: ${tableName}...`);
|
console.log(`Generating CRUD module for table: ${tableName}...`);
|
||||||
const names = this.getNamingConvention(tableName);
|
const names = this.getNamingConvention(tableName,columns);
|
||||||
const config = this.configService.get();
|
const config = this.configService.get();
|
||||||
const serverRoot = path.resolve(process.cwd(), config.server.path);
|
const serverRoot = path.resolve(process.cwd(), config.server.path);
|
||||||
const moduleDir = path.join(serverRoot, 'src', names.singular);
|
const moduleDir = path.join(serverRoot, 'src', names.singular);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.generateModuleFile(names, moduleDir);
|
await this.generateModuleFile(names, moduleDir);
|
||||||
await this.generateControllerFile(names, moduleDir);
|
await this.generateControllerFile(names, moduleDir);
|
||||||
@ -47,7 +51,10 @@ export class CrudGeneratorService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- MISSING METHODS NOW INCLUDED ---
|
private isStringBasedType(dbType: string): boolean {
|
||||||
|
return dbType.includes('char') || dbType.includes('text') || dbType === 'uuid';
|
||||||
|
}
|
||||||
|
|
||||||
private async generateModuleFile(names: NamingConvention, moduleDir: string) {
|
private async generateModuleFile(names: NamingConvention, moduleDir: string) {
|
||||||
const template = this.getModuleTemplate(names);
|
const template = this.getModuleTemplate(names);
|
||||||
const filePath = path.join(moduleDir, `${names.plural}.module.ts`);
|
const filePath = path.join(moduleDir, `${names.plural}.module.ts`);
|
||||||
@ -120,7 +127,7 @@ export class Query${names.pascal}Dto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getControllerTemplate(names: NamingConvention): string {
|
private getControllerTemplate(names: NamingConvention): string {
|
||||||
return `import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe } from '@nestjs/common';
|
return `import { Controller, Get, Post, Body, Patch, Param, Delete, Query, ParseIntPipe, DefaultValuePipe } from '@nestjs/common';
|
||||||
import { ${names.pascal}sService } from './${names.plural}.service';
|
import { ${names.pascal}sService } from './${names.plural}.service';
|
||||||
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
|
import { Create${names.pascal}Dto } from './dto/create-${names.singular}.dto';
|
||||||
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
|
import { Update${names.pascal}Dto } from './dto/update-${names.singular}.dto';
|
||||||
@ -139,12 +146,23 @@ export class ${names.pascal}sController {
|
|||||||
findAll(@Query() queryParams: Query${names.pascal}Dto) {
|
findAll(@Query() queryParams: Query${names.pascal}Dto) {
|
||||||
return this.${names.camel}sService.findAll(queryParams);
|
return this.${names.camel}sService.findAll(queryParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('search')
|
||||||
|
search(
|
||||||
|
@Query('q') term: string,
|
||||||
|
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
|
||||||
|
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
|
||||||
|
) {
|
||||||
|
return this.${names.camel}sService.search(term, { page, limit });
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
findOne(@Param('id', ParseIntPipe) id: number) {
|
findOne(@Param('id', ParseIntPipe) id: number) {
|
||||||
return this.${names.camel}sService.findOne(id);
|
return this.${names.camel}sService.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
update(@Param('id', ParseIntPipe) id: number, @Body() update${names.pascal}Dto: Update${names.pascal}Dto) {
|
update(@Param('id', ParseIntPipe) id: number, @Body() update${names.pascal}Dto: Update${names.pascal}Dto) {
|
||||||
return this.${names.camel}sService.update(id, update${names.pascal}Dto);
|
return this.${names.camel}sService.update(id, update${names.pascal}Dto);
|
||||||
@ -183,6 +201,10 @@ export class ${names.pascal}sService {
|
|||||||
@InjectRepository(${names.pascal})
|
@InjectRepository(${names.pascal})
|
||||||
private readonly ${names.camel}Repository: Repository<${names.pascal}>,
|
private readonly ${names.camel}Repository: Repository<${names.pascal}>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
private readonly searchableFields: (keyof ${names.pascal})[] = [
|
||||||
|
${names.searchableFields}
|
||||||
|
];
|
||||||
|
|
||||||
create(create${names.pascal}Dto: Create${names.pascal}Dto) {
|
create(create${names.pascal}Dto: Create${names.pascal}Dto) {
|
||||||
const newRecord = this.${names.camel}Repository.create(create${names.pascal}Dto);
|
const newRecord = this.${names.camel}Repository.create(create${names.pascal}Dto);
|
||||||
@ -249,6 +271,36 @@ export class ${names.pascal}sService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async search(term: string, options: { page: number; limit: number }) {
|
||||||
|
|
||||||
|
if (this.searchableFields.length === 0) {
|
||||||
|
console.warn('Search is not configured for this entity. Please populate the searchableFields array in the service.');
|
||||||
|
return { data: [], meta: { totalItems: 0, itemCount: 0, itemsPerPage: options.limit, totalPages: 0, currentPage: options.page } };
|
||||||
|
}
|
||||||
|
|
||||||
|
const whereConditions = this.searchableFields.map(field => ({
|
||||||
|
[field]: ILike('%'+term+'%'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const [data, totalItems] = await this.${names.camel}Repository.findAndCount({
|
||||||
|
where: whereConditions,
|
||||||
|
skip: (options.page - 1) * options.limit,
|
||||||
|
take: options.limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
totalItems,
|
||||||
|
itemCount: data.length,
|
||||||
|
itemsPerPage: options.limit,
|
||||||
|
totalPages: Math.ceil(totalItems / options.limit),
|
||||||
|
currentPage: options.page,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async findOne(id: number) {
|
async findOne(id: number) {
|
||||||
const record = await this.${names.camel}Repository.findOneBy({ id: id as any });
|
const record = await this.${names.camel}Repository.findOneBy({ id: id as any });
|
||||||
if (!record) {
|
if (!record) {
|
||||||
@ -288,13 +340,17 @@ export class ${names.pascal}sService {
|
|||||||
return `import { PartialType } from '@nestjs/mapped-types';\nimport { Create${names.pascal}Dto } from './create-${names.singular}.dto';\n\nexport class Update${names.pascal}Dto extends PartialType(Create${names.pascal}Dto) {}\n`;
|
return `import { PartialType } from '@nestjs/mapped-types';\nimport { Create${names.pascal}Dto } from './create-${names.singular}.dto';\n\nexport class Update${names.pascal}Dto extends PartialType(Create${names.pascal}Dto) {}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNamingConvention(tableName: string): NamingConvention {
|
private getNamingConvention(tableName: string, columns?: TableColumn[]): NamingConvention {
|
||||||
const singular = this.toSingular(tableName);
|
const singular = this.toSingular(tableName);
|
||||||
|
const searchableFields = (columns || [])
|
||||||
|
.filter(c => this.isStringBasedType(c.type))
|
||||||
|
.map(c => `'${c.name}'`); // Wrap in quotes for the array
|
||||||
return {
|
return {
|
||||||
singular: singular,
|
singular: singular,
|
||||||
plural: tableName,
|
plural: tableName,
|
||||||
pascal: this.toPascalCase(singular),
|
pascal: this.toPascalCase(singular),
|
||||||
camel: this.toCamelCase(singular),
|
camel: this.toCamelCase(singular),
|
||||||
|
searchableFields
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
86
src/services/generic-table-generator.service.ts
Normal file
86
src/services/generic-table-generator.service.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
// src/services/generic-list-generator.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { TemplateService } from './template.service';
|
||||||
|
import { TableColumn } from 'typeorm';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { promises as fsPromises } from 'fs';
|
||||||
|
|
||||||
|
interface FieldDefinition { name: string; tsType: string; }
|
||||||
|
interface NamingConvention { [key: string]: string; /*...*/ }
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GenericTableGeneratorService {
|
||||||
|
constructor(private readonly templateService: TemplateService) {}
|
||||||
|
|
||||||
|
public async generate(
|
||||||
|
tableName: string,
|
||||||
|
columns: TableColumn[],
|
||||||
|
names: NamingConvention,
|
||||||
|
featureDir: string,
|
||||||
|
): Promise<string> {
|
||||||
|
console.log(`Generating Generic Table View for ${tableName}...`);
|
||||||
|
|
||||||
|
const fields: FieldDefinition[] = columns.map(c => ({
|
||||||
|
name: c.name,
|
||||||
|
tsType: this.mapDbToTsType(c.type),
|
||||||
|
}));
|
||||||
|
|
||||||
|
names['columnDefinitions'] = this.buildColumnDefinitions(fields);
|
||||||
|
|
||||||
|
// Create folder: components/product-table
|
||||||
|
const compDir = path.join(featureDir, 'components', `${names.singular}-table`);
|
||||||
|
await fsPromises.mkdir(compDir, { recursive: true });
|
||||||
|
|
||||||
|
// Generate Data Provider
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(compDir, `${names.singular}-data-provider.service.ts`),
|
||||||
|
this.templateService.render('angular-generic/data-provider.service.ts.tpl', names),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate Table Component TS
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(compDir, `${names.singular}-table.component.ts`),
|
||||||
|
this.templateService.render('angular-generic/table.component.ts.tpl', names),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate Table Component HTML
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(compDir, `${names.singular}-table.component.html`),
|
||||||
|
this.templateService.render('angular-generic/table.component.html.tpl', names),
|
||||||
|
);
|
||||||
|
|
||||||
|
return path.join(compDir, `${names.singular}-table.component.ts`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildColumnDefinitions(fields: FieldDefinition[]): string {
|
||||||
|
return fields
|
||||||
|
.filter(f => f.name !== 'id')
|
||||||
|
.map(field => {
|
||||||
|
if (field.tsType === 'boolean') {
|
||||||
|
return ` {
|
||||||
|
attribute: '${field.name}',
|
||||||
|
headerCell: true,
|
||||||
|
valueCell: {
|
||||||
|
value: item => (item as any)?.${field.name} ? 'yes' : 'no',
|
||||||
|
},
|
||||||
|
},`;
|
||||||
|
}
|
||||||
|
return ` {
|
||||||
|
attribute: '${field.name}',
|
||||||
|
headerCell: true,
|
||||||
|
valueCell: true,
|
||||||
|
},`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private mapDbToTsType(dbType: string): string {
|
||||||
|
if (dbType.includes('int') || dbType.includes('serial')) return 'number';
|
||||||
|
if (['float', 'double', 'decimal', 'numeric', 'real'].includes(dbType)) return 'number';
|
||||||
|
if (dbType.includes('char') || dbType.includes('text') || dbType === 'uuid') return 'string';
|
||||||
|
if (dbType === 'boolean' || dbType === 'bool') return 'boolean';
|
||||||
|
if (dbType.includes('date') || dbType.includes('time')) return 'Date';
|
||||||
|
return 'any';
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/templates/angular-generic/data-provider.service.ts.tpl
Normal file
25
src/templates/angular-generic/data-provider.service.ts.tpl
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 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 { {{pascal}} } from '../../models/{{singular}}.model';
|
||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
import { {{pascal}}Service } from '../../services/{{singular}}.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class {{pascal}}DataProvider implements DataProvider<{{pascal}}> {
|
||||||
|
private {{camel}}Service = inject({{pascal}}Service);
|
||||||
|
|
||||||
|
getData(options?: GetDataOptions): Observable<GetDataResponse<{{pascal}}>> {
|
||||||
|
// The generic table's params are compatible with our NestJS Query DTO
|
||||||
|
return this.{{camel}}Service.find(options?.params ?? {}).pipe(
|
||||||
|
map((res) => {
|
||||||
|
// Adapt the paginated response to the GetDataResponse format
|
||||||
|
return { data: res };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/templates/angular-generic/table.component.html.tpl
Normal file
11
src/templates/angular-generic/table.component.html.tpl
Normal 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">{{title}}s (Generic Table)</h1>
|
||||||
|
<a routerLink="/{{plural}}/new" class="btn btn-primary">Create New</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<app-generic-table [config]="tableConfig"></app-generic-table>
|
||||||
|
</div>
|
||||||
96
src/templates/angular-generic/table.component.ts.tpl
Normal file
96
src/templates/angular-generic/table.component.ts.tpl
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
// 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 { {{pascal}} } from '../../models/{{singular}}.model';
|
||||||
|
import { {{pascal}}DataProvider } from './{{singular}}-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/{{singular}}.service';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-{{kebab}}-table',
|
||||||
|
standalone: true,
|
||||||
|
imports: [GenericTable, RouterModule],
|
||||||
|
templateUrl: './{{singular}}-table.component.html',
|
||||||
|
})
|
||||||
|
export class {{pascal}}TableComponent 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<{{pascal}}>;
|
||||||
|
|
||||||
|
{{camel}}DataProvider = inject({{pascal}}DataProvider);
|
||||||
|
{{camel}}Service = inject({{pascal}}Service);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const actionHandler = (action: ActionDefinition<{{pascal}}>, item: {{pascal}}) => {
|
||||||
|
switch (action.action) {
|
||||||
|
case 'view':
|
||||||
|
this.router.navigate(['/{{plural}}', item?.id]);
|
||||||
|
break;
|
||||||
|
case 'edit':
|
||||||
|
this.router.navigate(['/{{plural}}', 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.{{camel}}DataProvider,
|
||||||
|
columns: [
|
||||||
|
{{columnDefinitions}}
|
||||||
|
{
|
||||||
|
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<{{pascal}}>[],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnDefinition<{{pascal}}>[],
|
||||||
|
tableCssClass: '{{kebab}}-table-container',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteItem(id: number): void {
|
||||||
|
if (confirm('Are you sure you want to delete this item?')) {
|
||||||
|
this.{{camel}}Service.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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -7,6 +7,11 @@ import { Observable } from 'rxjs';
|
|||||||
import { {{pascal}}, PaginatedResponse } from '../models/{{singular}}.model';
|
import { {{pascal}}, PaginatedResponse } from '../models/{{singular}}.model';
|
||||||
import { ConfigurationService } from '../../../services/configuration.service';
|
import { ConfigurationService } from '../../../services/configuration.service';
|
||||||
|
|
||||||
|
export interface SearchResponse<T> {
|
||||||
|
data: T[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
@ -36,6 +41,18 @@ export class {{pascal}}Service {
|
|||||||
return this.http.get<PaginatedResponse<{{pascal}}>>(this.apiUrl, { params });
|
return this.http.get<PaginatedResponse<{{pascal}}>>(this.apiUrl, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search across multiple fields with a single term.
|
||||||
|
* @param term The search term (q).
|
||||||
|
*/
|
||||||
|
public search(term: string, page: number = 1, limit: number = 10): Observable<PaginatedResponse<{{pascal}}>> {
|
||||||
|
const params = new HttpParams()
|
||||||
|
.set('q', term)
|
||||||
|
.set('page', page.toString())
|
||||||
|
.set('limit', limit.toString());
|
||||||
|
return this.http.get<PaginatedResponse<{{pascal}}>>(`${this.apiUrl}/search`, { params });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a single record by its ID.
|
* Find a single record by its ID.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user