diff --git a/src/services/angular-generator.service.ts b/src/services/angular-generator.service.ts index 87979ba..6ab6d2c 100644 --- a/src/services/angular-generator.service.ts +++ b/src/services/angular-generator.service.ts @@ -8,8 +8,15 @@ import * as fs from 'fs'; import { promises as fsPromises } from 'fs'; import { TableColumn } from 'typeorm'; +// Interface for structured field metadata passed to templates +interface FieldDefinition { + name: string; + tsType: string; +} + +// Naming conventions used throughout the templates interface NamingConvention { - [key: string]: string; // This index signature makes it compatible with Record + [key: string]: string; singular: string; plural: string; pascal: string; @@ -30,20 +37,34 @@ export class AngularGeneratorService { 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); + const fields: FieldDefinition[] = (columns || []) + .map(c => ({ + name: c.name, + tsType: this.mapDbToTsType(c.type), + })); + names['modelProperties'] = fields.map(f => `${f.name}: ${f.tsType};`).join('\n '); + names['listHeaders'] = fields.map(f => `${f.name}`).join('\n '); + names['listCells'] = fields.map(f => `{{ item.${f.name} }}`).join('\n '); + names['detailsRows'] = fields.map(f => `\n ${f.name}\n {{ ${names.camel}.${f.name} }}\n `).join('\n '); + + const formFields = fields.filter(f => f.name !== 'id'); + names['formControls'] = formFields.map(f => `${f.name}: [null]`).join(',\n '); + names['formFields'] = formFields.map(f => this.getFormFieldHtml(f)).join('\n\n '); + + const filterableFields = fields.filter(f => f.tsType === 'string' && f.name !== 'id'); + names['filterFormControls'] = filterableFields.map(f => `${f.name}: ['']`).join(',\n '); + names['filterFormFields'] = filterableFields.map(f => this.getFormFieldHtml(f, true)).join('\n\n '); + + const numericFields = fields.filter(c => c.tsType === 'number' && c.name !== 'id').map(c => c.name); names['numericFieldsArray'] = JSON.stringify(numericFields); + names['colspan'] = (fields.length + 1).toString(); + 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); @@ -52,64 +73,55 @@ export class AngularGeneratorService { await this.generateDetailsComponent(names, featureDir); await this.generateFormComponent(names, featureDir); - const listComponentPath = path.join( - featureDir, - 'components', - `${names.singular}-list`, - `${names.singular}-list.component.ts`, - ); - - const detailsComponentPath = path.join( - featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts` - ); - + const listCompPath = path.join(featureDir, 'components', `${names.singular}-list`, `${names.singular}-list.component.ts`); + const detailsCompPath = path.join(featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts`); 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, - `${names.plural}/:id` - ); - - await this.moduleUpdaterService.addRouteToAngularApp( - `${names.pascal}ListComponent`, - listComponentPath, - names.plural, - ); + await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}DetailsComponent`, detailsCompPath, `${names.plural}/:id`); + await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}ListComponent`, listCompPath, names.plural); console.log(`✅ Angular files for "${tableName}" created successfully in: ${featureDir}`); - console.log('\n✨ app.routes.ts has been updated automatically with list and details routes! ✨'); + console.log('\n✨ app.routes.ts has been updated with full CRUD routes! ✨'); } catch (error) { console.error(`❌ An error occurred during Angular generation:`, error.message); } } - 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); + private getFormFieldHtml(field: FieldDefinition, isFilter: boolean = false): string { + const label = ``; + const placeholder = isFilter ? `placeholder="Filter by ${field.name}"` : ''; + let input = ''; - 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); + switch (field.tsType) { + case 'boolean': + input = ``; + return `
${input}
`; + case 'number': + input = ``; + break; + default: // string, Date, etc. + input = ``; } - 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); - const htmlContent = this.templateService.render('angular/details.component.html.tpl', names); - - await fsPromises.mkdir(compDir, { recursive: true }); - fs.writeFileSync(path.join(compDir, `${names.singular}-details.component.ts`), tsContent); - fs.writeFileSync(path.join(compDir, `${names.singular}-details.component.html`), htmlContent); + return `
${label}\n ${input}
`; } + 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'; + } + + // ... (All other generate... and naming methods are unchanged) private async generateModel(names: NamingConvention, featureDir: string) { const modelsDir = path.join(featureDir, 'models'); const content = this.templateService.render('angular/model.ts.tpl', names); @@ -117,7 +129,6 @@ export class AngularGeneratorService { await fsPromises.mkdir(modelsDir, { recursive: true }); fs.writeFileSync(filePath, content); } - private async generateService(names: NamingConvention, featureDir: string) { const servicesDir = path.join(featureDir, 'services'); const content = this.templateService.render('angular/service.ts.tpl', names); @@ -125,27 +136,38 @@ export class AngularGeneratorService { await fsPromises.mkdir(servicesDir, { recursive: true }); fs.writeFileSync(filePath, content); } - private async generateFilterComponent(names: NamingConvention, featureDir: string) { const compDir = path.join(featureDir, 'components', `${names.singular}-filter`); const tsContent = this.templateService.render('angular/filter.component.ts.tpl', names); const htmlContent = this.templateService.render('angular/filter.component.html.tpl', names); - await fsPromises.mkdir(compDir, { recursive: true }); fs.writeFileSync(path.join(compDir, `${names.singular}-filter.component.ts`), tsContent); fs.writeFileSync(path.join(compDir, `${names.singular}-filter.component.html`), htmlContent); } - private async generateListComponent(names: NamingConvention, featureDir: string) { const compDir = path.join(featureDir, 'components', `${names.singular}-list`); const tsContent = this.templateService.render('angular/list.component.ts.tpl', names); const htmlContent = this.templateService.render('angular/list.component.html.tpl', names); - await fsPromises.mkdir(compDir, { recursive: true }); fs.writeFileSync(path.join(compDir, `${names.singular}-list.component.ts`), tsContent); fs.writeFileSync(path.join(compDir, `${names.singular}-list.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); + const htmlContent = this.templateService.render('angular/details.component.html.tpl', names); + await fsPromises.mkdir(compDir, { recursive: true }); + fs.writeFileSync(path.join(compDir, `${names.singular}-details.component.ts`), tsContent); + fs.writeFileSync(path.join(compDir, `${names.singular}-details.component.html`), htmlContent); + } + 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 getNamingConvention(tableName: string): NamingConvention { const singular = this.toSingular(tableName); const pascal = this.toPascalCase(singular); @@ -158,8 +180,6 @@ export class AngularGeneratorService { kebab: singular, }; } - - // ... (rest of the naming utility functions are unchanged) private toSingular(name: string): string { if (name.endsWith('ies')) return name.slice(0, -3) + 'y'; return name.endsWith('s') ? name.slice(0, -1) : name; @@ -171,13 +191,4 @@ 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'; - } } \ No newline at end of file diff --git a/src/templates/angular/details.component.html.tpl b/src/templates/angular/details.component.html.tpl index 2339bec..80f0d2e 100644 --- a/src/templates/angular/details.component.html.tpl +++ b/src/templates/angular/details.component.html.tpl @@ -7,21 +7,10 @@

{{title}} Details

-
- - - - - - - - - - - + {{detailsRows}}
ID{{ {{camel}}.id }}
Name{{ {{camel}}.name }}
diff --git a/src/templates/angular/filter.component.html.tpl b/src/templates/angular/filter.component.html.tpl index 7041668..f94edc5 100644 --- a/src/templates/angular/filter.component.html.tpl +++ b/src/templates/angular/filter.component.html.tpl @@ -1,20 +1,11 @@ +
-
+
+{{filterFormFields}} - -
- - -
- - - - -
- -
-
+
+ +
+
\ No newline at end of file diff --git a/src/templates/angular/filter.component.ts.tpl b/src/templates/angular/filter.component.ts.tpl index 72923b4..42ff8e4 100644 --- a/src/templates/angular/filter.component.ts.tpl +++ b/src/templates/angular/filter.component.ts.tpl @@ -1,3 +1,5 @@ +// dvbooking-cli/src/templates/angular/filter.component.ts.tpl + // Generated by the CLI import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; @@ -15,15 +17,13 @@ export class {{pascal}}FilterComponent { constructor(private fb: FormBuilder) { this.filterForm = this.fb.group({ - name: [''], - // Add other filterable form controls here + {{filterFormControls}} }); this.filterForm.valueChanges.pipe( debounceTime(300), distinctUntilChanged() ).subscribe(values => { - // Remove empty properties before emitting const cleanFilter = Object.fromEntries( Object.entries(values).filter(([_, v]) => v != null && v !== '') ); diff --git a/src/templates/angular/form.component.html.tpl b/src/templates/angular/form.component.html.tpl index 067f42b..cbc8071 100644 --- a/src/templates/angular/form.component.html.tpl +++ b/src/templates/angular/form.component.html.tpl @@ -10,34 +10,7 @@
- -
- - -
- Name is required. -
-
- - -
- - -
- - -
- -
- - + {{formFields}}
Cancel diff --git a/src/templates/angular/form.component.ts.tpl b/src/templates/angular/form.component.ts.tpl index a74c741..5f7d685 100644 --- a/src/templates/angular/form.component.ts.tpl +++ b/src/templates/angular/form.component.ts.tpl @@ -30,10 +30,7 @@ export class {{pascal}}FormComponent implements OnInit { 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], + {{formControls}} }); } @@ -60,6 +57,7 @@ export class {{pascal}}FormComponent implements OnInit { onSubmit(): void { if (this.form.invalid) { + this.form.markAllAsTouched(); return; } diff --git a/src/templates/angular/list.component.html.tpl b/src/templates/angular/list.component.html.tpl index fd08c00..4633e4d 100644 --- a/src/templates/angular/list.component.html.tpl +++ b/src/templates/angular/list.component.html.tpl @@ -1,35 +1,34 @@ + +
-

{{title}}s

- Create New - +
+

{{title}}s

+ Create New +
+ -
- - - + {{listHeaders}} - - - + {{listCells}} - +
IDNameActions
{{ item.id }}{{ item.name }} - View - Edit + View + Edit
No {{plural}} found.No {{plural}} found.
diff --git a/src/templates/angular/model.ts.tpl b/src/templates/angular/model.ts.tpl index 0ee4916..fefabcb 100644 --- a/src/templates/angular/model.ts.tpl +++ b/src/templates/angular/model.ts.tpl @@ -1,8 +1,8 @@ +// dvbooking-cli/src/templates/angular/model.ts.tpl + // Generated by the CLI export interface {{pascal}} { - id: number; - name: string; - // Add other properties from your NestJS entity here + {{modelProperties}} } export interface PaginatedResponse {