generate angular form
This commit is contained in:
parent
fa06cb3de4
commit
620d42a1fd
@ -44,12 +44,18 @@ export class GenerateCommand extends CommandRunner {
|
||||
|
||||
case 'all':
|
||||
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 ---');
|
||||
await this.crudGeneratorService.generate(name);
|
||||
console.log('\n✨ All backend files generated successfully! ✨');
|
||||
console.log('\n--- Generating Angular Files ---');
|
||||
await this.angularGeneratorService.generate(name);
|
||||
await this.angularGeneratorService.generate(name,columns);
|
||||
console.log('\n✨ All files generated successfully! ✨');
|
||||
break;
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ import { ModuleUpdaterService } from './module-updater.service';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { TableColumn } from 'typeorm';
|
||||
|
||||
interface NamingConvention {
|
||||
[key: string]: string; // This index signature makes it compatible with Record<string, string>
|
||||
@ -25,20 +26,31 @@ export class AngularGeneratorService {
|
||||
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}...`);
|
||||
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 adminRoot = path.resolve(process.cwd(), config.admin.path);
|
||||
const featureDir = path.join(adminRoot, 'src', 'app', 'features', names.plural);
|
||||
|
||||
|
||||
try {
|
||||
await this.generateModel(names, featureDir);
|
||||
await this.generateService(names, featureDir);
|
||||
await this.generateFilterComponent(names, featureDir);
|
||||
await this.generateListComponent(names, featureDir);
|
||||
// NEW: Generate the details component
|
||||
await this.generateDetailsComponent(names, featureDir);
|
||||
await this.generateFormComponent(names, featureDir);
|
||||
|
||||
const listComponentPath = path.join(
|
||||
featureDir,
|
||||
@ -51,7 +63,13 @@ export class AngularGeneratorService {
|
||||
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(
|
||||
`${names.pascal}DetailsComponent`,
|
||||
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) {
|
||||
const compDir = path.join(featureDir, 'components', `${names.singular}-details`);
|
||||
const tsContent = this.templateService.render('angular/details.component.ts.tpl', names);
|
||||
@ -143,4 +171,13 @@ export class AngularGeneratorService {
|
||||
const pascal = this.toPascalCase(text);
|
||||
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';
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ export class EntityGeneratorService {
|
||||
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}...`);
|
||||
|
||||
const config = this.configService.get();
|
||||
@ -56,8 +56,11 @@ export class EntityGeneratorService {
|
||||
// Now the call will work correctly
|
||||
await this.moduleUpdaterService.addEntityToTypeOrm(className, outputPath);
|
||||
|
||||
return columns;
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ An error occurred:', error.message);
|
||||
return null;
|
||||
} finally {
|
||||
if (dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
|
||||
<div class="card-actions justify-end mt-6">
|
||||
<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>
|
||||
|
||||
51
src/templates/angular/form.component.html.tpl
Normal file
51
src/templates/angular/form.component.html.tpl
Normal 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>
|
||||
87
src/templates/angular/form.component.ts.tpl
Normal file
87
src/templates/angular/form.component.ts.tpl
Normal 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)
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
<!-- Generated by the CLI -->
|
||||
<div class="p-4 md:p-8 space-y-6">
|
||||
<h1 class="text-3xl font-bold">{{title}}s</h1>
|
||||
|
||||
<a routerLink="/{{plural}}/new" class="btn btn-primary">Create New</a>
|
||||
<!-- Filter Component -->
|
||||
<app-{{kebab}}-filter (filterChanged)="onFilterChanged($event)"></app-{{kebab}}-filter>
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
<!-- Add other table data cells here -->
|
||||
<td class="text-right space-x-2">
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user