@shivay_18/ng-crud 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.docx +0 -0
- package/README.md +142 -0
- package/dist/collection.json +10 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.html.template +48 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.scss.template +46 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.ts.template +121 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-delete-dialog.component.ts.template +27 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.html.template +75 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.scss.template +40 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.ts.template +95 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__.module.ts.template +73 -0
- package/dist/crud/files/module/models/__name@dasherize__.model.ts.template +4 -0
- package/dist/crud/files/module/services/__name@dasherize__.service.ts.template +42 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.html.template +48 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.scss.template +46 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.ts.template +142 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-delete-dialog.component.ts.template +30 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.html.template +75 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.scss.template +40 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.ts.template +121 -0
- package/dist/crud/files/standalone/models/__name@dasherize__.model.ts.template +4 -0
- package/dist/crud/files/standalone/services/__name@dasherize__.service.ts.template +42 -0
- package/dist/crud/index.d.ts +3 -0
- package/dist/crud/index.js +141 -0
- package/dist/crud/index.js.map +1 -0
- package/dist/crud/schema.d.ts +7 -0
- package/dist/crud/schema.js +3 -0
- package/dist/crud/schema.js.map +1 -0
- package/dist/crud/schema.json +34 -0
- package/package.json +33 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
3
|
+
import { Observable, throwError } from 'rxjs';
|
|
4
|
+
import { catchError } from 'rxjs/operators';
|
|
5
|
+
import { <%= classify(name) %> } from '../models/<%= dasherize(name) %>.model';
|
|
6
|
+
|
|
7
|
+
@Injectable({ providedIn: 'root' })
|
|
8
|
+
export class <%= classify(name) %>Service {
|
|
9
|
+
private apiUrl = '<%= apiUrl %>/<%= dasherize(name) %>s';
|
|
10
|
+
|
|
11
|
+
constructor(private http: HttpClient) {}
|
|
12
|
+
|
|
13
|
+
getAll(): Observable<<%= classify(name) %>[]> {
|
|
14
|
+
return this.http.get<<%= classify(name) %>[]>(this.apiUrl)
|
|
15
|
+
.pipe(catchError(this.handleError));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getById(id: number): Observable<<%= classify(name) %>> {
|
|
19
|
+
return this.http.get<<%= classify(name) %>>(`${this.apiUrl}/${id}`)
|
|
20
|
+
.pipe(catchError(this.handleError));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
create(item: <%= classify(name) %>): Observable<<%= classify(name) %>> {
|
|
24
|
+
return this.http.post<<%= classify(name) %>>(this.apiUrl, item)
|
|
25
|
+
.pipe(catchError(this.handleError));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(id: number, item: <%= classify(name) %>): Observable<<%= classify(name) %>> {
|
|
29
|
+
return this.http.put<<%= classify(name) %>>(`${this.apiUrl}/${id}`, item)
|
|
30
|
+
.pipe(catchError(this.handleError));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
delete(id: number): Observable<void> {
|
|
34
|
+
return this.http.delete<void>(`${this.apiUrl}/${id}`)
|
|
35
|
+
.pipe(catchError(this.handleError));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private handleError(error: HttpErrorResponse): Observable<never> {
|
|
39
|
+
console.error('HTTP Error:', error);
|
|
40
|
+
return throwError(() => error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="page-container">
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<button mat-icon-button (click)="goBack()">
|
|
5
|
+
<mat-icon>arrow_back</mat-icon>
|
|
6
|
+
</button>
|
|
7
|
+
<h2>{{ isEditMode ? 'Edit' : 'Add' }} <%= classify(name) %></h2>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div *ngIf="isLoading" class="spinner-wrap">
|
|
11
|
+
<mat-spinner diameter="40"></mat-spinner>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<mat-card *ngIf="!isLoading">
|
|
15
|
+
<mat-card-content>
|
|
16
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()" novalidate>
|
|
17
|
+
|
|
18
|
+
<% fields.forEach(function(field) { %>
|
|
19
|
+
<% if (field.control === 'checkbox') { %>
|
|
20
|
+
<div class="field-row">
|
|
21
|
+
<mat-checkbox formControlName="<%= field.name %>"><%= field.label %></mat-checkbox>
|
|
22
|
+
</div>
|
|
23
|
+
<% } else { %>
|
|
24
|
+
<mat-form-field appearance="outline" class="full-width">
|
|
25
|
+
<mat-label><%= field.label %></mat-label>
|
|
26
|
+
<input matInput type="<%= field.control %>" formControlName="<%= field.name %>" />
|
|
27
|
+
<mat-error *ngIf="form.get('<%= field.name %>')?.invalid && form.get('<%= field.name %>')?.touched">
|
|
28
|
+
<% if (field.required) { %> <span *ngIf="form.get('<%= field.name %>')?.hasError('required')">
|
|
29
|
+
<%= field.label %> is required.
|
|
30
|
+
</span>
|
|
31
|
+
<% } %> </mat-error>
|
|
32
|
+
</mat-form-field>
|
|
33
|
+
<% } %>
|
|
34
|
+
<% }); %>
|
|
35
|
+
|
|
36
|
+
<div class="form-actions">
|
|
37
|
+
<button mat-button type="button" (click)="goBack()" [disabled]="isSaving">Cancel</button>
|
|
38
|
+
<button mat-raised-button color="primary" type="submit" [disabled]="isSaving || form.invalid">
|
|
39
|
+
<mat-spinner *ngIf="isSaving" diameter="18" class="inline-spinner"></mat-spinner>
|
|
40
|
+
{{ isSaving ? 'Saving...' : (isEditMode ? 'Update' : 'Save') }}
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
</form>
|
|
45
|
+
</mat-card-content>
|
|
46
|
+
</mat-card>
|
|
47
|
+
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.page-container {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
max-width: 600px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.page-header {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
margin-bottom: 20px;
|
|
12
|
+
|
|
13
|
+
h2 {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 22px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.spinner-wrap {
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 48px 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.full-width {
|
|
27
|
+
width: 100%;
|
|
28
|
+
margin-bottom: 4px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.field-row {
|
|
32
|
+
margin-bottom: 16px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.form-actions {
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: flex-end;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
margin-top: 8px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.inline-spinner {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
margin-right: 6px;
|
|
45
|
+
vertical-align: middle;
|
|
46
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Component, OnInit } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
4
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
5
|
+
import { MatInputModule } from '@angular/material/input';
|
|
6
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
8
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
9
|
+
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
|
|
10
|
+
import { MatCardModule } from '@angular/material/card';
|
|
11
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
12
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
13
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
14
|
+
import { <%= classify(name) %>Service } from '../../services/<%= dasherize(name) %>.service';
|
|
15
|
+
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'app-<%= dasherize(name) %>-form',
|
|
18
|
+
standalone: true,
|
|
19
|
+
imports: [
|
|
20
|
+
CommonModule,
|
|
21
|
+
ReactiveFormsModule,
|
|
22
|
+
MatInputModule,
|
|
23
|
+
MatButtonModule,
|
|
24
|
+
MatFormFieldModule,
|
|
25
|
+
MatCheckboxModule,
|
|
26
|
+
MatSnackBarModule,
|
|
27
|
+
MatCardModule,
|
|
28
|
+
MatProgressSpinnerModule,
|
|
29
|
+
MatIconModule,
|
|
30
|
+
],
|
|
31
|
+
templateUrl: './<%= dasherize(name) %>-form.component.html',
|
|
32
|
+
styleUrls: ['./<%= dasherize(name) %>-form.component.scss'],
|
|
33
|
+
})
|
|
34
|
+
export class <%= classify(name) %>FormComponent implements OnInit {
|
|
35
|
+
form!: FormGroup;
|
|
36
|
+
isEditMode = false;
|
|
37
|
+
isLoading = false;
|
|
38
|
+
isSaving = false;
|
|
39
|
+
itemId: number | null = null;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private fb: FormBuilder,
|
|
43
|
+
private service: <%= classify(name) %>Service,
|
|
44
|
+
private router: Router,
|
|
45
|
+
private route: ActivatedRoute,
|
|
46
|
+
private snackBar: MatSnackBar
|
|
47
|
+
) {}
|
|
48
|
+
|
|
49
|
+
ngOnInit(): void {
|
|
50
|
+
this.buildForm();
|
|
51
|
+
const id = this.route.snapshot.paramMap.get('id');
|
|
52
|
+
if (id) {
|
|
53
|
+
this.isEditMode = true;
|
|
54
|
+
this.itemId = +id;
|
|
55
|
+
this.loadItem(this.itemId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
buildForm(): void {
|
|
60
|
+
this.form = this.fb.group({<% fields.forEach(function(field, i) { %>
|
|
61
|
+
<%= field.name %>: [<%= field.type === 'boolean' ? 'false' : field.type === 'number' ? 'null' : "''" %><% if (field.required) { %>, [Validators.required]<% } %>]<% if (i < fields.length - 1) { %>,<% } %><% }); %>
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loadItem(id: number): void {
|
|
66
|
+
this.isLoading = true;
|
|
67
|
+
this.service.getById(id).subscribe({
|
|
68
|
+
next: (item) => {
|
|
69
|
+
this.form.patchValue(item);
|
|
70
|
+
this.isLoading = false;
|
|
71
|
+
},
|
|
72
|
+
error: (error) => {
|
|
73
|
+
let errorMessage = 'Failed to load item.';
|
|
74
|
+
if (error.status === 404) {
|
|
75
|
+
errorMessage = '<%= classify(name) %> not found.';
|
|
76
|
+
this.router.navigate(['/<%= dasherize(name) %>']);
|
|
77
|
+
} else if (error.status === 500) {
|
|
78
|
+
errorMessage = 'Server error. Please try again later.';
|
|
79
|
+
}
|
|
80
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
81
|
+
this.isLoading = false;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
onSubmit(): void {
|
|
87
|
+
if (this.form.invalid) {
|
|
88
|
+
this.form.markAllAsTouched();
|
|
89
|
+
this.showValidationErrors();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.isSaving = true;
|
|
93
|
+
const value: <%= classify(name) %> = this.form.value;
|
|
94
|
+
const request = this.isEditMode
|
|
95
|
+
? this.service.update(this.itemId!, value)
|
|
96
|
+
: this.service.create(value);
|
|
97
|
+
|
|
98
|
+
request.subscribe({
|
|
99
|
+
next: () => {
|
|
100
|
+
this.snackBar.open(
|
|
101
|
+
`<%= classify(name) %> ${this.isEditMode ? 'updated' : 'created'} successfully.`,
|
|
102
|
+
'Close',
|
|
103
|
+
{ duration: 3000 }
|
|
104
|
+
);
|
|
105
|
+
this.isSaving = false;
|
|
106
|
+
this.goBack();
|
|
107
|
+
},
|
|
108
|
+
error: (error) => {
|
|
109
|
+
let errorMessage = 'Save failed. Please try again.';
|
|
110
|
+
if (error.status === 400 && error.error?.message) {
|
|
111
|
+
errorMessage = error.error.message;
|
|
112
|
+
} else if (error.status === 404) {
|
|
113
|
+
errorMessage = '<%= classify(name) %> not found.';
|
|
114
|
+
} else if (error.status === 500) {
|
|
115
|
+
errorMessage = 'Server error. Please try again later.';
|
|
116
|
+
}
|
|
117
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
|
118
|
+
this.isSaving = false;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private showValidationErrors(): void {
|
|
124
|
+
const errors: string[] = [];
|
|
125
|
+
Object.keys(this.form.controls).forEach(key => {
|
|
126
|
+
const control = this.form.get(key);
|
|
127
|
+
if (control?.invalid && control.errors) {
|
|
128
|
+
if (control.errors['required']) {
|
|
129
|
+
const fieldName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
130
|
+
errors.push(`${fieldName} is required`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
this.snackBar.open(errors.join(', '), 'Close', { duration: 4000 });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
goBack(): void {
|
|
140
|
+
this.router.navigate(['..'], { relativeTo: this.route });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Component, Inject } from '@angular/core';
|
|
2
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
3
|
+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
|
4
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
5
|
+
|
|
6
|
+
@Component({
|
|
7
|
+
selector: 'app-<%= dasherize(name) %>-delete-dialog',
|
|
8
|
+
standalone: true,
|
|
9
|
+
imports: [MatDialogModule, MatButtonModule],
|
|
10
|
+
template: `
|
|
11
|
+
<h2 mat-dialog-title>Delete <%= classify(name) %></h2>
|
|
12
|
+
<mat-dialog-content>
|
|
13
|
+
Are you sure you want to delete this record? This cannot be undone.
|
|
14
|
+
</mat-dialog-content>
|
|
15
|
+
<mat-dialog-actions align="end">
|
|
16
|
+
<button mat-button (click)="close(false)">Cancel</button>
|
|
17
|
+
<button mat-raised-button color="warn" (click)="close(true)">Delete</button>
|
|
18
|
+
</mat-dialog-actions>
|
|
19
|
+
`,
|
|
20
|
+
})
|
|
21
|
+
export class <%= classify(name) %>DeleteDialogComponent {
|
|
22
|
+
constructor(
|
|
23
|
+
public dialogRef: MatDialogRef<<%= classify(name) %>DeleteDialogComponent>,
|
|
24
|
+
@Inject(MAT_DIALOG_DATA) public data: <%= classify(name) %>
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
close(result: boolean): void {
|
|
28
|
+
this.dialogRef.close(result);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div class="page-container">
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2><%= classify(name) %> List</h2>
|
|
5
|
+
<button mat-raised-button color="primary" (click)="onCreate()">
|
|
6
|
+
<mat-icon>add</mat-icon> Add <%= classify(name) %>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<mat-card>
|
|
11
|
+
<mat-card-content>
|
|
12
|
+
|
|
13
|
+
<mat-form-field appearance="outline" class="search-box">
|
|
14
|
+
<mat-label>Search</mat-label>
|
|
15
|
+
<mat-icon matPrefix>search</mat-icon>
|
|
16
|
+
<input matInput (keyup)="applyFilter($event)" placeholder="Search..." />
|
|
17
|
+
</mat-form-field>
|
|
18
|
+
|
|
19
|
+
<div *ngIf="isLoading" class="spinner-wrap">
|
|
20
|
+
<mat-spinner diameter="40"></mat-spinner>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div [hidden]="isLoading">
|
|
24
|
+
<table mat-table [dataSource]="dataSource" matSort>
|
|
25
|
+
|
|
26
|
+
<ng-container matColumnDef="id">
|
|
27
|
+
<th mat-header-cell *matHeaderCellDef mat-sort-header>#</th>
|
|
28
|
+
<td mat-cell *matCellDef="let row">{{ row.id }}</td>
|
|
29
|
+
</ng-container>
|
|
30
|
+
|
|
31
|
+
<% fields.forEach(function(field) { %>
|
|
32
|
+
<ng-container matColumnDef="<%= field.name %>">
|
|
33
|
+
<th mat-header-cell *matHeaderCellDef mat-sort-header><%= field.label %></th>
|
|
34
|
+
<td mat-cell *matCellDef="let row">
|
|
35
|
+
<% if (field.type === 'boolean') { %>
|
|
36
|
+
<mat-chip [color]="row.<%= field.name %> ? 'primary' : ''" selected>
|
|
37
|
+
{{ row.<%= field.name %> ? 'Yes' : 'No' }}
|
|
38
|
+
</mat-chip>
|
|
39
|
+
<% } else { %>
|
|
40
|
+
{{ row.<%= field.name %> }}
|
|
41
|
+
<% } %>
|
|
42
|
+
</td>
|
|
43
|
+
</ng-container>
|
|
44
|
+
<% }); %>
|
|
45
|
+
|
|
46
|
+
<ng-container matColumnDef="actions">
|
|
47
|
+
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
|
48
|
+
<td mat-cell *matCellDef="let row">
|
|
49
|
+
<button mat-icon-button color="primary" (click)="onEdit(row)" matTooltip="Edit">
|
|
50
|
+
<mat-icon>edit</mat-icon>
|
|
51
|
+
</button>
|
|
52
|
+
<button mat-icon-button color="warn" (click)="onDelete(row)" matTooltip="Delete">
|
|
53
|
+
<mat-icon>delete</mat-icon>
|
|
54
|
+
</button>
|
|
55
|
+
</td>
|
|
56
|
+
</ng-container>
|
|
57
|
+
|
|
58
|
+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
59
|
+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
|
60
|
+
|
|
61
|
+
<tr class="mat-row" *matNoDataRow>
|
|
62
|
+
<td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
|
|
63
|
+
No records found.
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
<mat-paginator [pageSizeOptions]="[5, 10, 25]" showFirstLastButtons></mat-paginator>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
</mat-card-content>
|
|
73
|
+
</mat-card>
|
|
74
|
+
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.page-container {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
max-width: 1100px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.page-header {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
margin-bottom: 16px;
|
|
12
|
+
|
|
13
|
+
h2 {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 22px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.search-box {
|
|
21
|
+
width: 100%;
|
|
22
|
+
max-width: 360px;
|
|
23
|
+
margin-bottom: 16px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.spinner-wrap {
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: 48px 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
table {
|
|
33
|
+
width: 100%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.no-data {
|
|
37
|
+
text-align: center;
|
|
38
|
+
padding: 32px;
|
|
39
|
+
color: #888;
|
|
40
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Component, OnInit, ViewChild } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { Router, ActivatedRoute } from '@angular/router';
|
|
4
|
+
import { MatTableModule, MatTableDataSource } from '@angular/material/table';
|
|
5
|
+
import { MatPaginatorModule, MatPaginator } from '@angular/material/paginator';
|
|
6
|
+
import { MatSortModule, MatSort } from '@angular/material/sort';
|
|
7
|
+
import { MatInputModule } from '@angular/material/input';
|
|
8
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
9
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
10
|
+
import { MatDialogModule, MatDialog } from '@angular/material/dialog';
|
|
11
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
12
|
+
import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
|
|
13
|
+
import { MatCardModule } from '@angular/material/card';
|
|
14
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
15
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
16
|
+
import { MatChipsModule } from '@angular/material/chips';
|
|
17
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
18
|
+
import { <%= classify(name) %>Service } from '../../services/<%= dasherize(name) %>.service';
|
|
19
|
+
import { <%= classify(name) %>DeleteDialogComponent } from './<%= dasherize(name) %>-delete-dialog.component';
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'app-<%= dasherize(name) %>-list',
|
|
23
|
+
standalone: true,
|
|
24
|
+
imports: [
|
|
25
|
+
CommonModule,
|
|
26
|
+
MatTableModule,
|
|
27
|
+
MatPaginatorModule,
|
|
28
|
+
MatSortModule,
|
|
29
|
+
MatInputModule,
|
|
30
|
+
MatButtonModule,
|
|
31
|
+
MatIconModule,
|
|
32
|
+
MatDialogModule,
|
|
33
|
+
MatFormFieldModule,
|
|
34
|
+
MatSnackBarModule,
|
|
35
|
+
MatCardModule,
|
|
36
|
+
MatProgressSpinnerModule,
|
|
37
|
+
MatTooltipModule,
|
|
38
|
+
MatChipsModule,
|
|
39
|
+
],
|
|
40
|
+
templateUrl: './<%= dasherize(name) %>-list.component.html',
|
|
41
|
+
styleUrls: ['./<%= dasherize(name) %>-list.component.scss'],
|
|
42
|
+
})
|
|
43
|
+
export class <%= classify(name) %>ListComponent implements OnInit {
|
|
44
|
+
displayedColumns: string[] = ['id', <% fields.forEach(function(f, i) { %>'<%= f.name %>'<% if (i < fields.length - 1) { %>, <% } %><% }); %>, 'actions'];
|
|
45
|
+
dataSource = new MatTableDataSource<<%= classify(name) %>>();
|
|
46
|
+
isLoading = true;
|
|
47
|
+
|
|
48
|
+
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
49
|
+
@ViewChild(MatSort) sort!: MatSort;
|
|
50
|
+
|
|
51
|
+
constructor(
|
|
52
|
+
private service: <%= classify(name) %>Service,
|
|
53
|
+
private router: Router,
|
|
54
|
+
private route: ActivatedRoute,
|
|
55
|
+
private dialog: MatDialog,
|
|
56
|
+
private snackBar: MatSnackBar
|
|
57
|
+
) {}
|
|
58
|
+
|
|
59
|
+
ngOnInit(): void {
|
|
60
|
+
this.loadData();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
loadData(): void {
|
|
64
|
+
this.isLoading = true;
|
|
65
|
+
this.service.getAll().subscribe({
|
|
66
|
+
next: (data) => {
|
|
67
|
+
this.dataSource.data = data;
|
|
68
|
+
this.dataSource.paginator = this.paginator;
|
|
69
|
+
this.dataSource.sort = this.sort;
|
|
70
|
+
this.isLoading = false;
|
|
71
|
+
},
|
|
72
|
+
error: (error) => {
|
|
73
|
+
let errorMessage = 'Failed to load data.';
|
|
74
|
+
if (error.status === 500) {
|
|
75
|
+
errorMessage = 'Server error. Please try again later.';
|
|
76
|
+
} else if (error.status === 0) {
|
|
77
|
+
errorMessage = 'Unable to connect to server. Please check your connection.';
|
|
78
|
+
}
|
|
79
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
80
|
+
this.isLoading = false;
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
applyFilter(event: Event): void {
|
|
86
|
+
const filterValue = (event.target as HTMLInputElement).value;
|
|
87
|
+
this.dataSource.filter = filterValue.trim().toLowerCase();
|
|
88
|
+
if (this.dataSource.paginator) this.dataSource.paginator.firstPage();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onCreate(): void {
|
|
92
|
+
this.router.navigate(['new'], { relativeTo: this.route });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
onEdit(item: <%= classify(name) %>): void {
|
|
96
|
+
this.router.navigate([item.id, 'edit'], { relativeTo: this.route });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
onDelete(item: <%= classify(name) %>): void {
|
|
100
|
+
const ref = this.dialog.open(<%= classify(name) %>DeleteDialogComponent, { data: item, width: '400px' });
|
|
101
|
+
ref.afterClosed().subscribe((confirmed) => {
|
|
102
|
+
if (confirmed) {
|
|
103
|
+
this.service.delete(item.id!).subscribe({
|
|
104
|
+
next: () => {
|
|
105
|
+
this.snackBar.open('<%= classify(name) %> deleted successfully.', 'Close', { duration: 3000 });
|
|
106
|
+
this.loadData();
|
|
107
|
+
},
|
|
108
|
+
error: (error) => {
|
|
109
|
+
let errorMessage = 'Delete failed.';
|
|
110
|
+
if (error.status === 404) {
|
|
111
|
+
errorMessage = '<%= classify(name) %> not found or already deleted.';
|
|
112
|
+
} else if (error.status === 500) {
|
|
113
|
+
errorMessage = 'Server error. Please try again later.';
|
|
114
|
+
}
|
|
115
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
3
|
+
import { Observable, throwError } from 'rxjs';
|
|
4
|
+
import { catchError } from 'rxjs/operators';
|
|
5
|
+
import { <%= classify(name) %> } from '../models/<%= dasherize(name) %>.model';
|
|
6
|
+
|
|
7
|
+
@Injectable({ providedIn: 'root' })
|
|
8
|
+
export class <%= classify(name) %>Service {
|
|
9
|
+
private apiUrl = '<%= apiUrl %>/<%= dasherize(name) %>s';
|
|
10
|
+
|
|
11
|
+
constructor(private http: HttpClient) {}
|
|
12
|
+
|
|
13
|
+
getAll(): Observable<<%= classify(name) %>[]> {
|
|
14
|
+
return this.http.get<<%= classify(name) %>[]>(this.apiUrl)
|
|
15
|
+
.pipe(catchError(this.handleError));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getById(id: number): Observable<<%= classify(name) %>> {
|
|
19
|
+
return this.http.get<<%= classify(name) %>>(`${this.apiUrl}/${id}`)
|
|
20
|
+
.pipe(catchError(this.handleError));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
create(item: <%= classify(name) %>): Observable<<%= classify(name) %>> {
|
|
24
|
+
return this.http.post<<%= classify(name) %>>(this.apiUrl, item)
|
|
25
|
+
.pipe(catchError(this.handleError));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
update(id: number, item: <%= classify(name) %>): Observable<<%= classify(name) %>> {
|
|
29
|
+
return this.http.put<<%= classify(name) %>>(`${this.apiUrl}/${id}`, item)
|
|
30
|
+
.pipe(catchError(this.handleError));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
delete(id: number): Observable<void> {
|
|
34
|
+
return this.http.delete<void>(`${this.apiUrl}/${id}`)
|
|
35
|
+
.pipe(catchError(this.handleError));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private handleError(error: HttpErrorResponse): Observable<never> {
|
|
39
|
+
console.error('HTTP Error:', error);
|
|
40
|
+
return throwError(() => error);
|
|
41
|
+
}
|
|
42
|
+
}
|