generate angular form

This commit is contained in:
Roland Schneider 2025-11-19 10:53:18 +01:00
parent fa06cb3de4
commit 620d42a1fd
7 changed files with 193 additions and 9 deletions

View File

@ -44,12 +44,18 @@ export class GenerateCommand extends CommandRunner {
case 'all': case 'all':
console.log('--- Generating Entity ---'); console.log('--- Generating Entity ---');
await this.entityGeneratorService.generate(name); const columns = await this.entityGeneratorService.generate(name);
if (!columns) {
console.error('❌ Entity generation failed. Aborting subsequent steps.');
return;
}
console.log('\n--- Generating CRUD Module ---'); console.log('\n--- Generating CRUD Module ---');
await this.crudGeneratorService.generate(name); await this.crudGeneratorService.generate(name);
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); await this.angularGeneratorService.generate(name,columns);
console.log('\n✨ All files generated successfully! ✨'); console.log('\n✨ All files generated successfully! ✨');
break; break;

View File

@ -6,6 +6,7 @@ import { ModuleUpdaterService } from './module-updater.service';
import * as path from 'path'; 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';
interface NamingConvention { interface NamingConvention {
[key: string]: string; // This index signature makes it compatible with Record<string, string> [key: string]: string; // This index signature makes it compatible with Record<string, string>
@ -25,20 +26,31 @@ export class AngularGeneratorService {
private readonly moduleUpdaterService: ModuleUpdaterService, private readonly moduleUpdaterService: ModuleUpdaterService,
) {} ) {}
public async generate(tableName: string): Promise<void> { public async generate(tableName: string, columns?: TableColumn[]): Promise<void> {
console.log(`Generating Angular module for table: ${tableName}...`); console.log(`Generating Angular module for table: ${tableName}...`);
const names = this.getNamingConvention(tableName); const names = this.getNamingConvention(tableName);
const numericFields = (columns || [])
.filter(c => {
const tsType = this.mapDbToTsType(c.type);
return tsType === 'number' && !c.isPrimary;
})
.map(c => c.name);
names['numericFieldsArray'] = JSON.stringify(numericFields);
const config = this.configService.get(); const config = this.configService.get();
const adminRoot = path.resolve(process.cwd(), config.admin.path); const adminRoot = path.resolve(process.cwd(), config.admin.path);
const featureDir = path.join(adminRoot, 'src', 'app', 'features', names.plural); const featureDir = path.join(adminRoot, 'src', 'app', 'features', names.plural);
try { try {
await this.generateModel(names, featureDir); await this.generateModel(names, featureDir);
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);
// NEW: Generate the details component
await this.generateDetailsComponent(names, featureDir); await this.generateDetailsComponent(names, featureDir);
await this.generateFormComponent(names, featureDir);
const listComponentPath = path.join( const listComponentPath = path.join(
featureDir, featureDir,
@ -51,7 +63,13 @@ export class AngularGeneratorService {
featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts` featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts`
); );
// Add details route FIRST, as it's more specific (`/products/:id`) const formCompPath = path.join(featureDir, 'components', `${names.singular}-form`, `${names.singular}-form.component.ts`);
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( await this.moduleUpdaterService.addRouteToAngularApp(
`${names.pascal}DetailsComponent`, `${names.pascal}DetailsComponent`,
detailsComponentPath, detailsComponentPath,
@ -72,6 +90,16 @@ export class AngularGeneratorService {
} }
} }
private async generateFormComponent(names: NamingConvention, featureDir: string) {
const compDir = path.join(featureDir, 'components', `${names.singular}-form`);
const tsContent = this.templateService.render('angular/form.component.ts.tpl', names);
const htmlContent = this.templateService.render('angular/form.component.html.tpl', names);
await fsPromises.mkdir(compDir, { recursive: true });
fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.ts`), tsContent);
fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.html`), htmlContent);
}
private async generateDetailsComponent(names: NamingConvention, featureDir: string) { private async generateDetailsComponent(names: NamingConvention, featureDir: string) {
const compDir = path.join(featureDir, 'components', `${names.singular}-details`); const compDir = path.join(featureDir, 'components', `${names.singular}-details`);
const tsContent = this.templateService.render('angular/details.component.ts.tpl', names); const tsContent = this.templateService.render('angular/details.component.ts.tpl', names);
@ -143,4 +171,13 @@ export class AngularGeneratorService {
const pascal = this.toPascalCase(text); const pascal = this.toPascalCase(text);
return pascal.charAt(0).toLowerCase() + pascal.slice(1); return pascal.charAt(0).toLowerCase() + pascal.slice(1);
} }
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';
}
} }

View File

@ -13,7 +13,7 @@ export class EntityGeneratorService {
private readonly moduleUpdaterService: ModuleUpdaterService) {} private readonly moduleUpdaterService: ModuleUpdaterService) {}
public async generate(tableName: string): Promise<void> { public async generate(tableName: string): Promise<TableColumn[] | null> {
console.log(`Generating entity for table: ${tableName}...`); console.log(`Generating entity for table: ${tableName}...`);
const config = this.configService.get(); const config = this.configService.get();
@ -56,8 +56,11 @@ export class EntityGeneratorService {
// Now the call will work correctly // Now the call will work correctly
await this.moduleUpdaterService.addEntityToTypeOrm(className, outputPath); await this.moduleUpdaterService.addEntityToTypeOrm(className, outputPath);
return columns;
} catch (error) { } catch (error) {
console.error('❌ An error occurred:', error.message); console.error('❌ An error occurred:', error.message);
return null;
} finally { } finally {
if (dataSource.isInitialized) { if (dataSource.isInitialized) {
await dataSource.destroy(); await dataSource.destroy();

View File

@ -28,7 +28,7 @@
<div class="card-actions justify-end mt-6"> <div class="card-actions justify-end mt-6">
<a routerLink="/{{plural}}" class="btn btn-secondary">Back to List</a> <a routerLink="/{{plural}}" class="btn btn-secondary">Back to List</a>
<button class="btn btn-primary">Edit</button> <a routerLink="/{{plural}}/{{ {{camel}}.id }}/edit" class="btn btn-primary">Edit</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,51 @@
<!-- dvbooking-cli/src/templates/angular/form.component.html.tpl -->
<!-- Generated by the CLI -->
<div class="p-4 md:p-8">
<div class="card bg-base-100 shadow-xl max-w-2xl mx-auto">
<div class="card-body">
<h2 class="card-title text-3xl">
{{ isEditMode ? 'Edit' : 'Create' }} {{title}}
</h2>
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-4 mt-4">
<!-- Name Field -->
<div class="form-control">
<label class="label">
<span class="label-text">Name</span>
</label>
<input type="text" formControlName="name" class="input input-bordered w-full" />
<div *ngIf="form.get('name')?.invalid && form.get('name')?.touched" class="text-error text-sm mt-1">
Name is required.
</div>
</div>
<!-- Price Field -->
<div class="form-control">
<label class="label">
<span class="label-text">Price</span>
</label>
<input type="number" formControlName="price" class="input input-bordered w-full" />
</div>
<!-- Is Available Checkbox -->
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<span class="label-text">Is Available?</span>
<input type="checkbox" formControlName="is_available" class="checkbox" />
</label>
</div>
<!-- Add other form fields here -->
<div class="card-actions justify-end mt-6">
<a routerLink="/{{plural}}" class="btn btn-ghost">Cancel</a>
<button type="submit" class="btn btn-primary" [disabled]="form.invalid">
{{ isEditMode ? 'Update' : 'Create' }}
</button>
</div>
</form>
</div>
</div>
</div>

View File

@ -0,0 +1,87 @@
// dvbooking-cli/src/templates/angular/form.component.ts.tpl
// Generated by the CLI
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Observable, of } from 'rxjs';
import { switchMap, tap } from 'rxjs/operators';
import { {{pascal}} } from '../../models/{{singular}}.model';
import { {{pascal}}Service } from '../../services/{{singular}}.service';
@Component({
selector: 'app-{{kebab}}-form',
templateUrl: './{{singular}}-form.component.html',
standalone: true,
imports: [CommonModule, ReactiveFormsModule, RouterModule],
})
export class {{pascal}}FormComponent implements OnInit {
form: FormGroup;
isEditMode = false;
id: number | null = null;
private numericFields = {{numericFieldsArray}};
constructor(
private fb: FormBuilder,
private route: ActivatedRoute,
private router: Router,
private {{camel}}Service: {{pascal}}Service
) {
this.form = this.fb.group({
// Add your form controls here. Match them to your model/entity.
name: ['', Validators.required],
price: [null],
is_available: [true],
});
}
ngOnInit(): void {
this.route.params.pipe(
tap(params => {
if (params['id']) {
this.isEditMode = true;
this.id = +params['id'];
}
}),
switchMap(() => {
if (this.isEditMode && this.id) {
return this.{{camel}}Service.findOne(this.id);
}
return of(null);
})
).subscribe({{camel}} => {
if ({{camel}}) {
this.form.patchValue({{camel}});
}
});
}
onSubmit(): void {
if (this.form.invalid) {
return;
}
const payload = { ...this.form.value };
for (const field of this.numericFields) {
if (payload[field] != null && payload[field] !== '') {
payload[field] = parseFloat(payload[field]);
}
}
let action$: Observable<{{pascal}}>;
if (this.isEditMode && this.id) {
action$ = this.{{camel}}Service.update(this.id, payload);
} else {
action$ = this.{{camel}}Service.create(payload);
}
action$.subscribe({
next: () => this.router.navigate(['/{{plural}}']),
error: (err) => console.error('Failed to save {{singular}}', err)
});
}
}

View File

@ -1,7 +1,7 @@
<!-- Generated by the CLI --> <!-- Generated by the CLI -->
<div class="p-4 md:p-8 space-y-6"> <div class="p-4 md:p-8 space-y-6">
<h1 class="text-3xl font-bold">{{title}}s</h1> <h1 class="text-3xl font-bold">{{title}}s</h1>
<a routerLink="/{{plural}}/new" class="btn btn-primary">Create New</a>
<!-- Filter Component --> <!-- Filter Component -->
<app-{{kebab}}-filter (filterChanged)="onFilterChanged($event)"></app-{{kebab}}-filter> <app-{{kebab}}-filter (filterChanged)="onFilterChanged($event)"></app-{{kebab}}-filter>
@ -24,7 +24,7 @@
<!-- Add other table data cells here --> <!-- Add other table data cells here -->
<td class="text-right space-x-2"> <td class="text-right space-x-2">
<a [routerLink]="['/{{plural}}', item.id]" class="btn btn-sm btn-ghost">View</a> <a [routerLink]="['/{{plural}}', item.id]" class="btn btn-sm btn-ghost">View</a>
<button class="btn btn-sm btn-ghost">Edit</button> <a [routerLink]="['/{{plural}}', item.id, 'edit']" class="btn btn-sm btn-ghost">Edit</a>
<button (click)="deleteItem(item.id)" class="btn btn-sm btn-error btn-ghost">Delete</button> <button (click)="deleteItem(item.id)" class="btn btn-sm btn-error btn-ghost">Delete</button>
</td> </td>
</tr> </tr>