Skip to content

Commit

Permalink
Feature/upload file (#16)
Browse files Browse the repository at this point in the history
* Added upload functionality

* Added
- Mat-tooltip
- Filetype check
Updated CSS
Added testcases
Updated deployment file.(specific to main branch)
  • Loading branch information
Kuldeep-knoldus authored Jul 11, 2024
1 parent a43825a commit c8b6410
Show file tree
Hide file tree
Showing 17 changed files with 321 additions and 101 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/gcp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: GCP CI/CD Pipeline

on:
push:
branches:
- '**'
branches: [ "main" ]


env:
PROJECT_ID: ${{ secrets.GKE_PROJECT }}
Expand Down
3 changes: 2 additions & 1 deletion blogs-analyzer-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"bootstrap": "^5.3.0",
"highcharts": "^11.4.3",
"highcharts-angular": "^4.0.0",
"ngx-doc-viewer": "^15.0.1",
"ngx-logger": "^5.0.12",
"ngx-markdown": "^16.0.0",
"rxjs": "~7.8.0",
Expand All @@ -50,4 +51,4 @@
"sonar-scanner": "^3.1.0",
"typescript": "~5.0.2"
}
}
}
6 changes: 5 additions & 1 deletion blogs-analyzer-ui/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { HighchartsChartModule } from "highcharts-angular";
import { MarkdownModule } from "ngx-markdown";
import { LoggerModule, NgxLoggerLevel } from "ngx-logger";
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { NgxDocViewerModule } from "ngx-doc-viewer";
import { MatTooltipModule } from "@angular/material/tooltip";

@NgModule({
declarations: [
Expand All @@ -45,12 +47,14 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
MatButtonModule,
HighchartsChartModule,
MarkdownModule.forRoot(),
NgxDocViewerModule,
LoggerModule.forRoot({
level: NgxLoggerLevel.DEBUG,
serverLogLevel: NgxLoggerLevel.ERROR,
disableConsoleLogging: false
}),
NgbModule
NgbModule,
MatTooltipModule
],
providers: [],
bootstrap: [AppComponent]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
<button class="search-btn" (click)="getBlogById()">Search</button>
<div *ngIf="errorMessage && errorContext === 'id'" class="error-message">{{ errorMessage }}</div>
</div>
<div class="upload-container">
<input type="file" id="fileInput" (change)="onFileSelected($event)" hidden/>
<label for="fileInput" class="upload-label" matTooltip="Click to upload a .doc or .docx file">
<img src="assets/images/upload_file.svg" alt="upload doc file">
</label>
<div *ngIf="errorMessage" class="file-upload" [innerHTML]="errorMessage"></div>
</div>
</div>
<h2 class="mx-4">Existing Blogs</h2>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,11 @@
0 1px 1px 0 #9b9b9b,
0 1px 1px 0 #9b9b9b;
}

.upload-label {
cursor: pointer;
}

.file-upload {
color: red;
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,37 @@ describe('HomeComponent', () => {
expect(logger.error).toHaveBeenCalledWith(`Error fetching post by ID: ${mockError.message}`);
});

it('should handle valid .docx file in onFileSelected', () => {
const mockFile = new File([''], 'test.docx', {type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'});
const event = {target: {files: [mockFile]}} as unknown as Event;
const mockFileUrl = 'mock-url';

spyOn(window, 'FileReader').and.returnValue({
readAsDataURL: () => {
},
onload: (e: any) => {
e.target.result = mockFileUrl;
component.onFileSelected(event);
expect(component.fileUrl).toEqual(mockFileUrl);
expect(router.navigate).toHaveBeenCalledWith(['/quality-check'], {state: {url: mockFileUrl}});
}
} as any);

component.onFileSelected(event);
});

it('should handle invalid file type in onFileSelected', () => {
const mockFile = new File([''], 'test.pdf', {type: 'application/pdf'});
const event = {target: {files: [mockFile]}} as unknown as Event;
component.onFileSelected(event);
expect(component.errorMessage).toEqual('Invalid file type. <br> Please upload a .doc/.docx file.');
expect(router.navigate).not.toHaveBeenCalled();
});

it('should not set errorMessage if no file is selected', () => {
const event = {target: {files: []}} as unknown as Event;
component.onFileSelected(event);
expect(component.errorMessage).toBeNull();
expect(router.navigate).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class HomeComponent {
authorId!: number;
errorMessage: string | null = null;
errorContext: 'title' | 'author' | 'id' | null = null;
fileUrl: any

constructor(
private blogService: BlogService,
Expand Down Expand Up @@ -71,4 +72,26 @@ export class HomeComponent {
});
}
}

onFileSelected(event: Event): void {
const input = event.target as HTMLInputElement;

if (input.files?.[0]) {
const file = input.files[0];
const fileType = file.name.split('.').pop()?.toLowerCase();

if (fileType === 'doc' || fileType === 'docx') {
this.errorMessage = null;
const reader = new FileReader();

reader.onload = (e) => {
this.fileUrl = e.target?.result as string;
this.router.navigate(['/quality-check'], { state: { url: this.fileUrl } });
};
reader.readAsDataURL(file);
} else {
this.errorMessage = `Invalid file type. <br> Please upload a .doc/.docx file.`;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('TabularViewComponent', () => {
serverLogLevel: NgxLoggerLevel.ERROR
})],
providers: [
{provide: NGXLogger, useValue: loggerSpy}
{ provide: NGXLogger, useValue: loggerSpy }
],
schemas: [NO_ERRORS_SCHEMA]
});
Expand Down Expand Up @@ -99,7 +99,81 @@ describe('TabularViewComponent', () => {

expect(component.loading).toBeFalse();
expect(component.errorMessage).toContain('Failed to fetch Data');
expect(component.logger.error).toHaveBeenCalledWith('Error fetching posts for page 1: Failed to fetch posts');
expect(logger.error).toHaveBeenCalledWith('Error fetching posts for page 1: Failed to fetch posts');
});

it('should initialize column definitions', () => {
expect(component.columnDefs.length).toBeGreaterThan(0);
expect(component.columnDefs[0].headerName).toBe('Blog ID');
});

it('should emit click event', () => {
spyOn(component.clickEvent, 'emit');
component.clickEvent.emit(1);

expect(component.clickEvent.emit).toHaveBeenCalledWith(1);
});

it('should handle quality check button click', () => {
const mockResponse = { id: 1, title: 'Test Post' };
spyOn(component.blogService, 'getPostById').and.returnValue(of(mockResponse));
spyOn(component.router, 'navigate');

const params = { data: { id: 1 } };
component.columnDefs.find(col => col.field === 'id' && col.headerName === 'Quality Check').onCellClicked(params);

expect(logger.debug).toHaveBeenCalledWith('Initiating quality check for blog ID: 1');
expect(component.router.navigate).toHaveBeenCalledWith(['/quality-check'], { state: { data: mockResponse } });
});

it('should handle error during quality check button click', () => {
const mockError = new Error('Failed to fetch post by ID');
spyOn(component.blogService, 'getPostById').and.returnValue(throwError(mockError));

const params = { data: { id: 1 } };
component.columnDefs.find(col => col.field === 'id' && col.headerName === 'Quality Check').onCellClicked(params);

expect(logger.error).toHaveBeenCalledWith('Error fetching post by ID 1: Failed to fetch post by ID');
expect(component.errorMessage).toContain('Failed to fetch Data');
});

it('should have loading state initially', () => {
expect(component.loading).toBeTrue();
});

it('should update loading state after fetchPosts call', () => {
const mockData = { posts: [], totalPages: 1, isLastPage: true };
spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData));

component.fetchPosts(1);

expect(component.loading).toBeFalse();
});

it('should set isLastPage correctly', () => {
const mockData = { posts: [], totalPages: 1, isLastPage: true };
spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData));

component.fetchPosts(1);

expect(component.isLastPage).toBeTrue();
});

it('should set totalPages correctly', () => {
const mockData = { posts: [], totalPages: 3, isLastPage: false };
spyOn(component.blogService, 'getAllPosts').and.returnValue(of(mockData));

component.fetchPosts(1);

expect(component.totalPages).toBe(3);
});

it('should handle click event for view button', () => {
spyOn(window, 'open');

const params = { data: { url: 'http://example.com' } };
component.columnDefs.find(col => col.field === 'url' && col.headerName === 'View').onCellClicked(params);

expect(window.open).toHaveBeenCalledWith('http://example.com', '_blank');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { NGXLogger } from 'ngx-logger';
styleUrls: ['./tabular-view.component.scss']
})
export class TabularViewComponent implements OnInit {
protected columnDefs: any[];
columnDefs: any[];
rowData: any[];
loading: boolean = true;
currentPage: number = 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,62 @@
Back
</button>

<div class="container">
<ng-container *ngIf="initialLoading;">
<div class="spinner mt-5" data-testid="mabl-bulk-shrink-loader">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
</div>
</ng-container>

<div class="container" *ngIf="!initialLoading">
<div class="blog-content">
<h2>HTML Preview</h2>
<mat-card class="blog-card">
<div [innerHTML]="postData"></div>
<mat-card class="blog-card" *ngIf="fileUrl; else noFileContent">
<div #docViewer class="doc-viewer-container">
<ngx-doc-viewer [url]="fileUrl" viewer="mammoth"></ngx-doc-viewer>
</div>
</mat-card>
<ng-template #noFileContent>
<mat-card class="blog-card">
<div [innerHTML]="postData"></div>
</mat-card>
</ng-template>
</div>
<button class="search-btn" (click)="checkQuality()">Check Blog's Quality</button>
<div *ngIf="errorMessage" class="alert alert-danger mt-3" role="alert">
{{ errorMessage }}
</div>
<div *ngIf="qualityResults.length > 0">
<div class="result-box">
<h3>Overall Rating:</h3>
<span class="rating">{{ overallRating | number:'1.1-1' }}</span><br>
<ngb-rating class="stars" [max]="5" [(rate)]="overallRating" [readonly]="true"></ngb-rating>
<h3>Overall Feedback:</h3>
<p>{{ overallFeedback }}</p>

<button class="search-btn" (click)="checkQuality()" [disabled]="isLoading">Check Blog's Quality</button>

<ng-container *ngIf="isLoading; else dataLoaded">
<div class="spinner mt-5" data-testid="mabl-bulk-shrink-loader">
<div class="spinner-border text-primary" role="status">
<span class="sr-only"></span>
</div>
</div>
</ng-container>

<ng-template #dataLoaded>
<div *ngIf="errorMessage" class="alert alert-danger mt-3" role="alert">
<div [innerHTML]="errorMessage"></div>
</div>
<div class="charts-grid">
<div *ngFor="let result of qualityResults" class="chart-item">
<app-report
[actualLabel]="[result.originalLabel]"
[oppositeLabel]="[result.oppositeLabel]"
[chartData]="[result.value]"
[chartTitle]="result.originalLabel">
</app-report>
<div class="comment" [innerHTML]="result.comment | markdown"></div>
<div *ngIf="qualityResults.length > 0">
<div class="result-box">
<h3>Overall Rating:</h3>
<span class="rating">{{ overallRating | number:'1.1-1' }}</span><br>
<ngb-rating class="stars" [max]="5" [(rate)]="overallRating" [readonly]="true"></ngb-rating>
<h3>Overall Feedback:</h3>
<p>{{ overallFeedback }}</p>
</div>
<div class="charts-grid">
<div *ngFor="let result of qualityResults" class="chart-item">
<app-report
[actualLabel]="[result.originalLabel]"
[oppositeLabel]="[result.oppositeLabel]"
[chartData]="[result.value]"
[chartTitle]="result.originalLabel">
</app-report>
<div class="comment" [innerHTML]="result.comment | markdown"></div>
</div>
</div>
</div>
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,10 @@ textarea {
.ngb-rating .star.empty {
color: #d3d3d3;
}

.spinner {
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
Loading

0 comments on commit c8b6410

Please sign in to comment.