Skip to content

Commit 547a74a

Browse files
committed
feat: upload CSV file
1 parent 5f6ab34 commit 547a74a

11 files changed

+192
-4
lines changed

src/app/admin/admin.module.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,18 @@ import { MatTableModule } from '@angular/material/table';
77
import { OrdersService } from './orders/orders.service';
88
import { ManageProductsComponent } from './manage-products/manage-products.component';
99
import { MatButtonModule } from '@angular/material/button';
10+
import { FilePickerModule } from '../shared/file-picker/file-picker.module';
11+
import { ManageProductsService } from './manage-products/manage-products.service';
1012

1113
@NgModule({
1214
declarations: [OrdersComponent, ManageProductsComponent],
13-
imports: [CommonModule, AdminRoutingModule, MatTableModule, MatButtonModule],
14-
providers: [OrdersService],
15+
imports: [
16+
CommonModule,
17+
AdminRoutingModule,
18+
MatTableModule,
19+
MatButtonModule,
20+
FilePickerModule,
21+
],
22+
providers: [OrdersService, ManageProductsService],
1523
})
1624
export class AdminModule {}

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

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

3+
<app-file-picker
4+
[(file)]="selectedFile"
5+
(uploadClick)="onUploadCSV()"
6+
></app-file-picker>
7+
38
<table class="w-100" [dataSource]="products$" mat-table>
49
<ng-container matColumnDef="from">
510
<th mat-header-cell *matHeaderCellDef>Title</th>
Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
22
import { Observable } from 'rxjs';
33
import { Product } from '../../products/product.interface';
44
import { ProductsService } from '../../products/products.service';
5+
import { ManageProductsService } from './manage-products.service';
56

67
@Component({
78
selector: 'app-manage-products',
@@ -11,11 +12,30 @@ import { ProductsService } from '../../products/products.service';
1112
export class ManageProductsComponent implements OnInit {
1213
readonly columns = ['from', 'description', 'price', 'count', 'action'];
1314

15+
selectedFile: File | null = null;
16+
1417
products$!: Observable<Product[]>;
1518

16-
constructor(private readonly productsService: ProductsService) {}
19+
constructor(
20+
private readonly productsService: ProductsService,
21+
private readonly manageProductsService: ManageProductsService,
22+
private readonly cdr: ChangeDetectorRef
23+
) {}
1724

1825
ngOnInit(): void {
1926
this.products$ = this.productsService.getProducts();
2027
}
28+
29+
onUploadCSV(): void {
30+
if (!this.selectedFile) {
31+
return;
32+
}
33+
34+
this.manageProductsService
35+
.uploadProductsCSV(this.selectedFile)
36+
.subscribe(() => {
37+
this.selectedFile = null;
38+
this.cdr.markForCheck();
39+
});
40+
}
2141
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { TestBed } from '@angular/core/testing';
2+
3+
import { ManageProductsService } from './manage-products.service';
4+
5+
describe('ManageProductsService', () => {
6+
let service: ManageProductsService;
7+
8+
beforeEach(() => {
9+
TestBed.configureTestingModule({});
10+
service = TestBed.inject(ManageProductsService);
11+
});
12+
13+
it('should be created', () => {
14+
expect(service).toBeTruthy();
15+
});
16+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { Injectable, Injector } from '@angular/core';
2+
import { EMPTY, Observable } from 'rxjs';
3+
import { ApiService } from '../../core/api.service';
4+
import { switchMap } from 'rxjs/operators';
5+
6+
@Injectable()
7+
export class ManageProductsService extends ApiService {
8+
constructor(injector: Injector) {
9+
super(injector);
10+
}
11+
12+
uploadProductsCSV(file: File): Observable<unknown> {
13+
if (!this.endpointEnabled('import')) {
14+
console.warn(
15+
'Endpoint "import" is disabled. To enable change your environment.ts config'
16+
);
17+
return EMPTY;
18+
}
19+
20+
return this.getPreSignedUrl(file.name).pipe(
21+
switchMap((url) =>
22+
this.http.put(url, file, {
23+
headers: {
24+
// eslint-disable-next-line @typescript-eslint/naming-convention
25+
'Content-Type': 'text/csv',
26+
},
27+
})
28+
)
29+
);
30+
}
31+
32+
private getPreSignedUrl(fileName: string): Observable<string> {
33+
const url = this.getUrl('import', 'import');
34+
35+
return this.http.get<string>(url, {
36+
params: {
37+
name: fileName,
38+
},
39+
});
40+
}
41+
}

src/app/products/products.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { ApiService } from '../core/api.service';
1010
export class ProductsService extends ApiService {
1111
getProducts(): Observable<Product[]> {
1212
if (!this.endpointEnabled('bff')) {
13+
console.warn(
14+
'Endpoint "bff" is disabled. To enable change your environment.ts config'
15+
);
1316
return this.http.get<Product[]>('/assets/products.json');
1417
}
1518

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<input
2+
#fileInput
3+
(change)="selectFile(fileInput.files)"
4+
accept="text/csv"
5+
class="d-none"
6+
type="file"
7+
/>
8+
9+
<div class="h5 text-muted d-flex align-items-center">
10+
<ng-container *ngIf="file; else selectFileTemplate">
11+
<span class="mr-3">Selected file: {{ file.name }}</span>
12+
13+
<button (click)="uploadClick.emit()" color="primary" mat-flat-button>
14+
Upload
15+
</button>
16+
<button (click)="removeFile()" color="warn" mat-flat-button>Delete</button>
17+
</ng-container>
18+
</div>
19+
20+
<ng-template #selectFileTemplate>
21+
<span class="mr-3">Select file to upload</span>
22+
<button (click)="fileInput.click()" color="accent" mat-flat-button>
23+
Select file
24+
</button>
25+
</ng-template>

src/app/shared/file-picker/file-picker.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 { FilePickerComponent } from './file-picker.component';
4+
5+
describe('FilePickerComponent', () => {
6+
let component: FilePickerComponent;
7+
let fixture: ComponentFixture<FilePickerComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
declarations: [FilePickerComponent],
12+
}).compileComponents();
13+
});
14+
15+
beforeEach(() => {
16+
fixture = TestBed.createComponent(FilePickerComponent);
17+
component = fixture.componentInstance;
18+
fixture.detectChanges();
19+
});
20+
21+
it('should create', () => {
22+
expect(component).toBeTruthy();
23+
});
24+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Component, EventEmitter, Input, Output } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-file-picker',
5+
templateUrl: './file-picker.component.html',
6+
styleUrls: ['./file-picker.component.scss'],
7+
})
8+
export class FilePickerComponent {
9+
@Input() file: File | null = null;
10+
11+
@Output() fileChange = new EventEmitter<File | null>();
12+
@Output() uploadClick = new EventEmitter<void>();
13+
14+
selectFile(files: FileList | null): void {
15+
if (!files?.length) {
16+
this.removeFile();
17+
return;
18+
}
19+
20+
const file = files.item(0) as File;
21+
22+
if (!['text/csv', 'application/vnd.ms-excel'].includes(file.type)) {
23+
this.removeFile();
24+
return;
25+
}
26+
27+
this.fileChange.emit(file);
28+
this.file = file;
29+
}
30+
31+
removeFile(): void {
32+
this.file = null;
33+
this.fileChange.emit(null);
34+
}
35+
}

0 commit comments

Comments
 (0)