Skip to content

Commit 52ceb01

Browse files
serguntchikSergey Volkov
andauthored
feat: add the missing product edit module (#25)
* feat: add the missing product edit module * feat: add the missing product edit module (review fixes) * feat: add the missing product edit module (review fixes, part 2) * feat: add the missing product edit module (review fixes, part 3) Co-authored-by: Sergey Volkov <[email protected]>
1 parent b4a2028 commit 52ceb01

File tree

9 files changed

+322
-13
lines changed

9 files changed

+322
-13
lines changed

src/app/admin/admin-routing.module.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
22
import { RouterModule, Routes } from '@angular/router';
33
import { OrdersComponent } from './orders/orders.component';
44
import { ManageProductsComponent } from './manage-products/manage-products.component';
5+
import { EditProductComponent } from './edit-product/edit-product.component';
56

67
const routes: Routes = [
78
{
@@ -12,6 +13,14 @@ const routes: Routes = [
1213
path: 'products',
1314
component: ManageProductsComponent,
1415
},
16+
{
17+
path: 'products/new',
18+
component: EditProductComponent,
19+
},
20+
{
21+
path: 'products/:productId',
22+
component: EditProductComponent,
23+
},
1524
];
1625

1726
@NgModule({

src/app/admin/admin.module.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { NgModule } from '@angular/core';
22
import { CommonModule } from '@angular/common';
3+
import { ReactiveFormsModule } from '@angular/forms';
4+
import { MatCardModule } from '@angular/material/card';
5+
import { MatInputModule } from '@angular/material/input';
6+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
37

48
import { AdminRoutingModule } from './admin-routing.module';
59
import { OrdersComponent } from './orders/orders.component';
@@ -9,15 +13,24 @@ import { ManageProductsComponent } from './manage-products/manage-products.compo
913
import { MatButtonModule } from '@angular/material/button';
1014
import { FilePickerModule } from '../shared/file-picker/file-picker.module';
1115
import { ManageProductsService } from './manage-products/manage-products.service';
16+
import { EditProductComponent } from './edit-product/edit-product.component';
1217

1318
@NgModule({
14-
declarations: [OrdersComponent, ManageProductsComponent],
19+
declarations: [
20+
OrdersComponent,
21+
ManageProductsComponent,
22+
EditProductComponent,
23+
],
1524
imports: [
16-
CommonModule,
1725
AdminRoutingModule,
26+
CommonModule,
27+
FilePickerModule,
28+
MatCardModule,
29+
MatInputModule,
1830
MatTableModule,
1931
MatButtonModule,
20-
FilePickerModule,
32+
MatProgressSpinnerModule,
33+
ReactiveFormsModule,
2134
],
2235
providers: [OrdersService, ManageProductsService],
2336
})
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<mat-card *ngIf="loaded$ | async; else loaderTemplate">
2+
<h1 class="text-center pt-4" mat-card-title>
3+
{{ productId ? "Edit product" : "Create new product" }}
4+
</h1>
5+
6+
<mat-card-content>
7+
<form class="row" [formGroup]="form">
8+
<div class="col-12">
9+
<mat-form-field class="w-100">
10+
<mat-label>Title</mat-label>
11+
<input matInput formControlName="title" />
12+
<mat-error
13+
*ngIf="titleCtrl.touched && titleCtrl.hasError('required')"
14+
>
15+
Product title is required
16+
</mat-error>
17+
</mat-form-field>
18+
</div>
19+
20+
<div class="col-12">
21+
<mat-form-field class="w-100">
22+
<mat-label>Description</mat-label>
23+
<textarea matInput formControlName="description"></textarea>
24+
<mat-error
25+
*ngIf="
26+
descriptionCtrl.touched && descriptionCtrl.hasError('required')
27+
"
28+
>
29+
Product description is required
30+
</mat-error>
31+
</mat-form-field>
32+
</div>
33+
34+
<div class="col col-md-6">
35+
<mat-form-field class="w-100">
36+
<mat-label>Price ($)</mat-label>
37+
<input type="number" matInput formControlName="price" />
38+
<mat-error
39+
*ngIf="priceCtrl.touched && priceCtrl.hasError('required')"
40+
>
41+
Product price is required
42+
</mat-error>
43+
</mat-form-field>
44+
</div>
45+
46+
<div class="col col-md-6">
47+
<mat-form-field class="w-100">
48+
<mat-label>Count</mat-label>
49+
<input type="number" matInput formControlName="count" />
50+
<mat-error
51+
*ngIf="countCtrl.touched && countCtrl.hasError('required')"
52+
>
53+
Product count is required
54+
</mat-error>
55+
</mat-form-field>
56+
</div>
57+
</form>
58+
</mat-card-content>
59+
60+
<mat-card-actions>
61+
<button class="text-uppercase mr-2" mat-flat-button routerLink="..">
62+
cancel
63+
</button>
64+
<button
65+
class="text-uppercase"
66+
color="primary"
67+
mat-flat-button
68+
[disabled]="form.invalid || requestInProgress"
69+
(click)="editProduct()"
70+
>
71+
save product
72+
</button>
73+
</mat-card-actions>
74+
</mat-card>
75+
76+
<ng-template #loaderTemplate>
77+
<div class="py-5">
78+
<mat-spinner [diameter]="40" class="mx-auto"></mat-spinner>
79+
</div>
80+
</ng-template>

src/app/admin/edit-product/edit-product.component.scss

Whitespace-only changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { EditProductComponent } from './edit-product.component';
4+
5+
describe('EditProductComponent', () => {
6+
let component: EditProductComponent;
7+
let fixture: ComponentFixture<EditProductComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [EditProductComponent],
12+
}).compileComponents();
13+
});
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(EditProductComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import {
2+
ChangeDetectionStrategy,
3+
Component,
4+
OnDestroy,
5+
OnInit,
6+
} from '@angular/core';
7+
import {
8+
AbstractControl,
9+
FormBuilder,
10+
FormGroup,
11+
Validators,
12+
} from '@angular/forms';
13+
import { ActivatedRoute, Router } from '@angular/router';
14+
15+
import { BehaviorSubject, Subject } from 'rxjs';
16+
import { finalize, takeUntil } from 'rxjs/operators';
17+
18+
import { Product } from '../../products/product.interface';
19+
import { ProductsService } from '../../products/products.service';
20+
import { NotificationService } from '../../core/notification.service';
21+
22+
@Component({
23+
selector: 'app-edit-product',
24+
templateUrl: './edit-product.component.html',
25+
styleUrls: ['./edit-product.component.scss'],
26+
changeDetection: ChangeDetectionStrategy.OnPush,
27+
})
28+
export class EditProductComponent implements OnInit, OnDestroy {
29+
form: FormGroup;
30+
productId: string | null = null;
31+
requestInProgress = false;
32+
33+
loaded$ = new BehaviorSubject(false);
34+
35+
get countCtrl(): AbstractControl {
36+
return this.form.get('count') as AbstractControl;
37+
}
38+
get descriptionCtrl(): AbstractControl {
39+
return this.form.get('description') as AbstractControl;
40+
}
41+
get priceCtrl(): AbstractControl {
42+
return this.form.get('price') as AbstractControl;
43+
}
44+
get titleCtrl(): AbstractControl {
45+
return this.form.get('title') as AbstractControl;
46+
}
47+
48+
private readonly onDestroy$: Subject<void> = new Subject();
49+
50+
constructor(
51+
private readonly activatedRoute: ActivatedRoute,
52+
private readonly fb: FormBuilder,
53+
private readonly notificationService: NotificationService,
54+
private readonly productsService: ProductsService,
55+
private readonly router: Router
56+
) {
57+
this.form = this.fb.group({
58+
title: ['', Validators.required],
59+
description: ['', Validators.required],
60+
price: ['', Validators.required],
61+
count: ['', Validators.required],
62+
});
63+
}
64+
65+
ngOnInit(): void {
66+
const productId = this.activatedRoute.snapshot.paramMap.get('productId');
67+
68+
if (!productId) {
69+
this.loaded$.next(true);
70+
return;
71+
}
72+
73+
this.productsService
74+
.getProductById(productId)
75+
.pipe(
76+
finalize(() => this.loaded$.next(true)),
77+
takeUntil(this.onDestroy$)
78+
)
79+
.subscribe((product) => {
80+
if (product) {
81+
this.form.patchValue(product);
82+
this.productId = product.id;
83+
}
84+
});
85+
}
86+
87+
ngOnDestroy(): void {
88+
this.onDestroy$.next();
89+
this.onDestroy$.complete();
90+
}
91+
92+
editProduct(): void {
93+
const product: Product = this.form.value;
94+
if (!product) {
95+
return;
96+
}
97+
98+
const editProduct$ = this.productId
99+
? this.productsService.editProduct(this.productId, product)
100+
: this.productsService.createNewProduct(product);
101+
102+
this.requestInProgress = true;
103+
editProduct$.subscribe(
104+
() => this.router.navigate(['../'], { relativeTo: this.activatedRoute }),
105+
(error: unknown) => {
106+
console.warn(error);
107+
this.requestInProgress = false;
108+
this.notificationService.showError(
109+
`Failed to ${this.productId ? 'edit' : 'create'} product`
110+
);
111+
}
112+
);
113+
}
114+
}

src/app/admin/manage-products/manage-products.component.html

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
<h1>Manage products</h1>
22

3-
<app-file-picker
4-
[(file)]="selectedFile"
5-
(uploadClick)="onUploadCSV()"
6-
></app-file-picker>
3+
<div class="d-flex">
4+
<app-file-picker
5+
class="mr-2"
6+
[(file)]="selectedFile"
7+
(uploadClick)="onUploadCSV()"
8+
></app-file-picker>
9+
10+
<button
11+
class="text-uppercase"
12+
color="primary"
13+
mat-flat-button
14+
routerLink="new"
15+
>
16+
create product
17+
</button>
18+
</div>
719

820
<table class="w-100" [dataSource]="products$" mat-table>
921
<ng-container matColumnDef="from">
@@ -29,7 +41,12 @@ <h1>Manage products</h1>
2941
<ng-container matColumnDef="action">
3042
<th mat-header-cell *matHeaderCellDef>Action</th>
3143
<td mat-cell *matCellDef="let product">
32-
<button class="text-uppercase mr-2" color="primary" mat-flat-button>
44+
<button
45+
class="text-uppercase mr-2"
46+
color="primary"
47+
mat-flat-button
48+
[routerLink]="product.id"
49+
>
3350
manage
3451
</button>
3552
<button class="text-uppercase" color="warn" mat-flat-button>

src/app/products/products.service.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,60 @@
11
import { Injectable } from '@angular/core';
2-
import { Observable, of } from 'rxjs';
3-
import { Product } from './product.interface';
2+
3+
import { EMPTY, Observable, of, throwError } from 'rxjs';
44
import { map } from 'rxjs/operators';
5+
6+
import { Product } from './product.interface';
7+
58
import { ApiService } from '../core/api.service';
69

710
@Injectable({
811
providedIn: 'root',
912
})
1013
export class ProductsService extends ApiService {
14+
createNewProduct(product: Product): Observable<Product> {
15+
if (!this.endpointEnabled('bff')) {
16+
console.warn(
17+
'Endpoint "bff" is disabled. To enable change your environment.ts config'
18+
);
19+
return EMPTY;
20+
}
21+
22+
const url = this.getUrl('bff', 'products');
23+
return this.http.post<Product>(url, product);
24+
}
25+
26+
editProduct(id: string, changedProduct: Product): Observable<Product> {
27+
if (!this.endpointEnabled('bff')) {
28+
console.warn(
29+
'Endpoint "bff" is disabled. To enable change your environment.ts config'
30+
);
31+
return EMPTY;
32+
}
33+
34+
const url = this.getUrl('bff', `products/${id}`);
35+
return this.http.put<Product>(url, changedProduct);
36+
}
37+
38+
getProductById(id: string): Observable<Product | null> {
39+
if (!this.endpointEnabled('bff')) {
40+
console.warn(
41+
'Endpoint "bff" is disabled. To enable change your environment.ts config'
42+
);
43+
return this.http
44+
.get<Product[]>('/assets/products.json')
45+
.pipe(
46+
map(
47+
(products) => products.find((product) => product.id === id) || null
48+
)
49+
);
50+
}
51+
52+
const url = this.getUrl('bff', `products/${id}`);
53+
return this.http
54+
.get<{ product: Product }>(url)
55+
.pipe(map((resp) => resp.product));
56+
}
57+
1158
getProducts(): Observable<Product[]> {
1259
if (!this.endpointEnabled('bff')) {
1360
console.warn(

0 commit comments

Comments
 (0)