generate angular with dynamic columns
This commit is contained in:
parent
620d42a1fd
commit
a6d495545a
@ -8,8 +8,15 @@ 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';
|
||||||
|
|
||||||
|
// Interface for structured field metadata passed to templates
|
||||||
|
interface FieldDefinition {
|
||||||
|
name: string;
|
||||||
|
tsType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naming conventions used throughout the templates
|
||||||
interface NamingConvention {
|
interface NamingConvention {
|
||||||
[key: string]: string; // This index signature makes it compatible with Record<string, string>
|
[key: string]: string;
|
||||||
singular: string;
|
singular: string;
|
||||||
plural: string;
|
plural: string;
|
||||||
pascal: string;
|
pascal: string;
|
||||||
@ -30,20 +37,34 @@ export class AngularGeneratorService {
|
|||||||
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 || [])
|
const fields: FieldDefinition[] = (columns || [])
|
||||||
.filter(c => {
|
.map(c => ({
|
||||||
const tsType = this.mapDbToTsType(c.type);
|
name: c.name,
|
||||||
return tsType === 'number' && !c.isPrimary;
|
tsType: this.mapDbToTsType(c.type),
|
||||||
})
|
}));
|
||||||
.map(c => c.name);
|
|
||||||
|
|
||||||
|
names['modelProperties'] = fields.map(f => `${f.name}: ${f.tsType};`).join('\n ');
|
||||||
|
names['listHeaders'] = fields.map(f => `<th>${f.name}</th>`).join('\n ');
|
||||||
|
names['listCells'] = fields.map(f => `<td>{{ item.${f.name} }}</td>`).join('\n ');
|
||||||
|
names['detailsRows'] = fields.map(f => `<tr>\n <th>${f.name}</th>\n <td>{{ ${names.camel}.${f.name} }}</td>\n </tr>`).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['numericFieldsArray'] = JSON.stringify(numericFields);
|
||||||
|
|
||||||
|
names['colspan'] = (fields.length + 1).toString();
|
||||||
|
|
||||||
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);
|
||||||
@ -52,64 +73,55 @@ export class AngularGeneratorService {
|
|||||||
await this.generateDetailsComponent(names, featureDir);
|
await this.generateDetailsComponent(names, featureDir);
|
||||||
await this.generateFormComponent(names, featureDir);
|
await this.generateFormComponent(names, featureDir);
|
||||||
|
|
||||||
const listComponentPath = path.join(
|
const listCompPath = path.join(featureDir, 'components', `${names.singular}-list`, `${names.singular}-list.component.ts`);
|
||||||
featureDir,
|
const detailsCompPath = path.join(featureDir, 'components', `${names.singular}-details`, `${names.singular}-details.component.ts`);
|
||||||
'components',
|
|
||||||
`${names.singular}-list`,
|
|
||||||
`${names.singular}-list.component.ts`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const detailsComponentPath = 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`);
|
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}/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(
|
await this.moduleUpdaterService.addRouteToAngularApp(`${names.pascal}ListComponent`, listCompPath, names.plural);
|
||||||
`${names.pascal}DetailsComponent`,
|
|
||||||
detailsComponentPath,
|
|
||||||
`${names.plural}/:id`
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.moduleUpdaterService.addRouteToAngularApp(
|
|
||||||
`${names.pascal}ListComponent`,
|
|
||||||
listComponentPath,
|
|
||||||
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 automatically with list and details routes! ✨');
|
console.log('\n✨ app.routes.ts has been updated with full CRUD routes! ✨');
|
||||||
|
|
||||||
} 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 generateFormComponent(names: NamingConvention, featureDir: string) {
|
private getFormFieldHtml(field: FieldDefinition, isFilter: boolean = false): string {
|
||||||
const compDir = path.join(featureDir, 'components', `${names.singular}-form`);
|
const label = `<label class="label"><span class="label-text">${field.name}</span></label>`;
|
||||||
const tsContent = this.templateService.render('angular/form.component.ts.tpl', names);
|
const placeholder = isFilter ? `placeholder="Filter by ${field.name}"` : '';
|
||||||
const htmlContent = this.templateService.render('angular/form.component.html.tpl', names);
|
let input = '';
|
||||||
|
|
||||||
await fsPromises.mkdir(compDir, { recursive: true });
|
switch (field.tsType) {
|
||||||
fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.ts`), tsContent);
|
case 'boolean':
|
||||||
fs.writeFileSync(path.join(compDir, `${names.singular}-form.component.html`), htmlContent);
|
input = `<label class="label cursor-pointer justify-start gap-4">
|
||||||
|
<span class="label-text">${field.name}</span>
|
||||||
|
<input type="checkbox" formControlName="${field.name}" class="checkbox" />
|
||||||
|
</label>`;
|
||||||
|
return `<div class="form-control">${input}</div>`;
|
||||||
|
case 'number':
|
||||||
|
input = `<input type="number" formControlName="${field.name}" class="input input-bordered w-full" ${placeholder} />`;
|
||||||
|
break;
|
||||||
|
default: // string, Date, etc.
|
||||||
|
input = `<input type="text" formControlName="${field.name}" class="input input-bordered w-full" ${placeholder} />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateDetailsComponent(names: NamingConvention, featureDir: string) {
|
return `<div class="form-control">${label}\n ${input}</div>`;
|
||||||
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 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) {
|
private async generateModel(names: NamingConvention, featureDir: string) {
|
||||||
const modelsDir = path.join(featureDir, 'models');
|
const modelsDir = path.join(featureDir, 'models');
|
||||||
const content = this.templateService.render('angular/model.ts.tpl', names);
|
const content = this.templateService.render('angular/model.ts.tpl', names);
|
||||||
@ -117,7 +129,6 @@ export class AngularGeneratorService {
|
|||||||
await fsPromises.mkdir(modelsDir, { recursive: true });
|
await fsPromises.mkdir(modelsDir, { recursive: true });
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateService(names: NamingConvention, featureDir: string) {
|
private async generateService(names: NamingConvention, featureDir: string) {
|
||||||
const servicesDir = path.join(featureDir, 'services');
|
const servicesDir = path.join(featureDir, 'services');
|
||||||
const content = this.templateService.render('angular/service.ts.tpl', names);
|
const content = this.templateService.render('angular/service.ts.tpl', names);
|
||||||
@ -125,27 +136,38 @@ export class AngularGeneratorService {
|
|||||||
await fsPromises.mkdir(servicesDir, { recursive: true });
|
await fsPromises.mkdir(servicesDir, { recursive: true });
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateFilterComponent(names: NamingConvention, featureDir: string) {
|
private async generateFilterComponent(names: NamingConvention, featureDir: string) {
|
||||||
const compDir = path.join(featureDir, 'components', `${names.singular}-filter`);
|
const compDir = path.join(featureDir, 'components', `${names.singular}-filter`);
|
||||||
const tsContent = this.templateService.render('angular/filter.component.ts.tpl', names);
|
const tsContent = this.templateService.render('angular/filter.component.ts.tpl', names);
|
||||||
const htmlContent = this.templateService.render('angular/filter.component.html.tpl', names);
|
const htmlContent = this.templateService.render('angular/filter.component.html.tpl', names);
|
||||||
|
|
||||||
await fsPromises.mkdir(compDir, { recursive: true });
|
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.ts`), tsContent);
|
||||||
fs.writeFileSync(path.join(compDir, `${names.singular}-filter.component.html`), htmlContent);
|
fs.writeFileSync(path.join(compDir, `${names.singular}-filter.component.html`), htmlContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateListComponent(names: NamingConvention, featureDir: string) {
|
private async generateListComponent(names: NamingConvention, featureDir: string) {
|
||||||
const compDir = path.join(featureDir, 'components', `${names.singular}-list`);
|
const compDir = path.join(featureDir, 'components', `${names.singular}-list`);
|
||||||
const tsContent = this.templateService.render('angular/list.component.ts.tpl', names);
|
const tsContent = this.templateService.render('angular/list.component.ts.tpl', names);
|
||||||
const htmlContent = this.templateService.render('angular/list.component.html.tpl', names);
|
const htmlContent = this.templateService.render('angular/list.component.html.tpl', names);
|
||||||
|
|
||||||
await fsPromises.mkdir(compDir, { recursive: true });
|
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.ts`), tsContent);
|
||||||
fs.writeFileSync(path.join(compDir, `${names.singular}-list.component.html`), htmlContent);
|
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 {
|
private getNamingConvention(tableName: string): NamingConvention {
|
||||||
const singular = this.toSingular(tableName);
|
const singular = this.toSingular(tableName);
|
||||||
const pascal = this.toPascalCase(singular);
|
const pascal = this.toPascalCase(singular);
|
||||||
@ -158,8 +180,6 @@ export class AngularGeneratorService {
|
|||||||
kebab: singular,
|
kebab: singular,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (rest of the naming utility functions are unchanged)
|
|
||||||
private toSingular(name: string): string {
|
private toSingular(name: string): string {
|
||||||
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
|
if (name.endsWith('ies')) return name.slice(0, -3) + 'y';
|
||||||
return name.endsWith('s') ? name.slice(0, -1) : name;
|
return name.endsWith('s') ? name.slice(0, -1) : name;
|
||||||
@ -171,13 +191,4 @@ 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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -7,21 +7,10 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title text-3xl">{{title}} Details</h2>
|
<h2 class="card-title text-3xl">{{title}} Details</h2>
|
||||||
|
|
||||||
<!-- Details List -->
|
|
||||||
<div class="overflow-x-auto mt-4">
|
<div class="overflow-x-auto mt-4">
|
||||||
<table class="table w-full">
|
<table class="table w-full">
|
||||||
<tbody>
|
<tbody>
|
||||||
<!-- Row for ID -->
|
{{detailsRows}}
|
||||||
<tr>
|
|
||||||
<th class="w-1/3">ID</th>
|
|
||||||
<td>{{ {{camel}}.id }}</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Row for Name -->
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<td>{{ {{camel}}.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<!-- Add more rows for other properties here -->
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,20 +1,11 @@
|
|||||||
|
<!-- dvbooking-cli/src/templates/angular/filter.component.html.tpl -->
|
||||||
<!-- Generated by the CLI -->
|
<!-- Generated by the CLI -->
|
||||||
<form [formGroup]="filterForm" class="p-4 bg-base-200 rounded-lg shadow">
|
<form [formGroup]="filterForm" class="p-4 bg-base-200 rounded-lg shadow">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 items-end">
|
||||||
|
{{filterFormFields}}
|
||||||
|
|
||||||
<!-- Filter for 'name' -->
|
<div class="form-control">
|
||||||
<div class="form-control">
|
|
||||||
<label class="label">
|
|
||||||
<span class="label-text">Name</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" formControlName="name" placeholder="Filter by name" class="input input-bordered w-full">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add other filter inputs here -->
|
|
||||||
|
|
||||||
<!-- Reset Button -->
|
|
||||||
<div class="form-control">
|
|
||||||
<button type="button" (click)="reset()" class="btn btn-secondary">Reset</button>
|
<button type="button" (click)="reset()" class="btn btn-secondary">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
// dvbooking-cli/src/templates/angular/filter.component.ts.tpl
|
||||||
|
|
||||||
// Generated by the CLI
|
// Generated by the CLI
|
||||||
import { Component, EventEmitter, Output } from '@angular/core';
|
import { Component, EventEmitter, Output } from '@angular/core';
|
||||||
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
||||||
@ -15,15 +17,13 @@ export class {{pascal}}FilterComponent {
|
|||||||
|
|
||||||
constructor(private fb: FormBuilder) {
|
constructor(private fb: FormBuilder) {
|
||||||
this.filterForm = this.fb.group({
|
this.filterForm = this.fb.group({
|
||||||
name: [''],
|
{{filterFormControls}}
|
||||||
// Add other filterable form controls here
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filterForm.valueChanges.pipe(
|
this.filterForm.valueChanges.pipe(
|
||||||
debounceTime(300),
|
debounceTime(300),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
).subscribe(values => {
|
).subscribe(values => {
|
||||||
// Remove empty properties before emitting
|
|
||||||
const cleanFilter = Object.fromEntries(
|
const cleanFilter = Object.fromEntries(
|
||||||
Object.entries(values).filter(([_, v]) => v != null && v !== '')
|
Object.entries(values).filter(([_, v]) => v != null && v !== '')
|
||||||
);
|
);
|
||||||
|
|||||||
@ -10,34 +10,7 @@
|
|||||||
|
|
||||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-4 mt-4">
|
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="space-y-4 mt-4">
|
||||||
|
|
||||||
<!-- Name Field -->
|
{{formFields}}
|
||||||
<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">
|
<div class="card-actions justify-end mt-6">
|
||||||
<a routerLink="/{{plural}}" class="btn btn-ghost">Cancel</a>
|
<a routerLink="/{{plural}}" class="btn btn-ghost">Cancel</a>
|
||||||
|
|||||||
@ -30,10 +30,7 @@ export class {{pascal}}FormComponent implements OnInit {
|
|||||||
private {{camel}}Service: {{pascal}}Service
|
private {{camel}}Service: {{pascal}}Service
|
||||||
) {
|
) {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
// Add your form controls here. Match them to your model/entity.
|
{{formControls}}
|
||||||
name: ['', Validators.required],
|
|
||||||
price: [null],
|
|
||||||
is_available: [true],
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +57,7 @@ export class {{pascal}}FormComponent implements OnInit {
|
|||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
if (this.form.invalid) {
|
if (this.form.invalid) {
|
||||||
|
this.form.markAllAsTouched();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,26 @@
|
|||||||
|
<!-- dvbooking-cli/src/templates/angular/list.component.html.tpl -->
|
||||||
|
|
||||||
<!-- 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">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
<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>
|
<a routerLink="/{{plural}}/new" class="btn btn-primary">Create New</a>
|
||||||
<!-- Filter Component -->
|
</div>
|
||||||
|
|
||||||
<app-{{kebab}}-filter (filterChanged)="onFilterChanged($event)"></app-{{kebab}}-filter>
|
<app-{{kebab}}-filter (filterChanged)="onFilterChanged($event)"></app-{{kebab}}-filter>
|
||||||
|
|
||||||
<!-- Data Table -->
|
|
||||||
<ng-container *ngIf="paginatedResponse$ | async as response; else loading">
|
<ng-container *ngIf="paginatedResponse$ | async as response; else loading">
|
||||||
<div class="overflow-x-auto bg-base-100 rounded-lg shadow">
|
<div class="overflow-x-auto bg-base-100 rounded-lg shadow">
|
||||||
<table class="table w-full">
|
<table class="table w-full">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
{{listHeaders}}
|
||||||
<th>Name</th>
|
|
||||||
<!-- Add other table headers here -->
|
|
||||||
<th class="text-right">Actions</th>
|
<th class="text-right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let item of response.data" class="hover">
|
<tr *ngFor="let item of response.data" class="hover">
|
||||||
<td>{{ item.id }}</td>
|
{{listCells}}
|
||||||
<td>{{ item.name }}</td>
|
|
||||||
<!-- 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>
|
||||||
<a [routerLink]="['/{{plural}}', item.id, 'edit']" class="btn btn-sm btn-ghost">Edit</a>
|
<a [routerLink]="['/{{plural}}', item.id, 'edit']" class="btn btn-sm btn-ghost">Edit</a>
|
||||||
@ -29,7 +28,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr *ngIf="response.data.length === 0">
|
<tr *ngIf="response.data.length === 0">
|
||||||
<td colspan="3" class="text-center">No {{plural}} found.</td>
|
<td colspan="{{colspan}}" class="text-center">No {{plural}} found.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
|
// dvbooking-cli/src/templates/angular/model.ts.tpl
|
||||||
|
|
||||||
// Generated by the CLI
|
// Generated by the CLI
|
||||||
export interface {{pascal}} {
|
export interface {{pascal}} {
|
||||||
id: number;
|
{{modelProperties}}
|
||||||
name: string;
|
|
||||||
// Add other properties from your NestJS entity here
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaginatedResponse<T> {
|
export interface PaginatedResponse<T> {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user