better toasts, better dashboard, first implementation of consumer status

This commit is contained in:
Jonas Winkler
2020-11-07 12:05:15 +01:00
parent d46203c114
commit fae7ae06eb
28 changed files with 450 additions and 134 deletions

View File

@@ -5,7 +5,7 @@ import { Observable } from 'rxjs';
import { MATCHING_ALGORITHMS } from 'src/app/data/matching-model';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
@Directive()
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
@@ -66,7 +66,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> implements OnI
this.activeModal.close()
this.success.emit(result)
}, error => {
this.toastService.showToast(Toast.makeError(`Could not save ${this.entityName}: ${error.error.name}`))
this.toastService.showError(`Could not save ${this.entityName}: ${error.error.name}`)
})
}

View File

@@ -1,7 +1,8 @@
<ngb-toast
*ngFor="let toast of toasts"
[header]="toast.title" [autohide]="true" [delay]="toast.delay"
[header]="toast.title" [autohide]="true" [delay]="toast.delay || 5000"
[class]="toast.classname"
(hide)="toastService.closeToast(toast)">
{{toast.content}}
<p>{{toast.content}}</p>
<p *ngIf="toast.action"><button class="btn btn-sm btn-outline-secondary" (click)="toastService.closeToast(toast); toast.action()">{{toast.actionName}}</button></p>
</ngb-toast>

View File

@@ -6,59 +6,11 @@
<div class='row'>
<div class="col-lg">
<ng-container *ngFor="let v of savedDashboardViews">
<h4>{{v.viewConfig.title}}</h4>
<table class="table table-sm table-hover table-borderless">
<thead>
<tr>
<th>Date created</th>
<th scope="col">Document</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let doc of v.documents" routerLink="/documents/{{doc.id}}">
<td>{{doc.created | date}}</td>
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag>
</tr>
</tbody>
</table>
</ng-container>
<ng-container *ngIf="savedDashboardViews.length == 0">
<h4>Saved views</h4>
<p>This space is reserved to display your saved views. Go to your documents and save a view to have it displayed here!</p>
</ng-container>
<app-saved-view-widget [viewConfig]="conf" *ngFor="let conf of savedViewConfigService.getDashboardConfigs()"></app-saved-view-widget>
</div>
<div class="col-lg">
<h4>Statistics</h4>
<p>Documents in inbox: {{statistics.documents_inbox}}</p>
<p>Total documents: {{statistics.documents_total}}</p>
<h4>Upload new Document</h4>
<form>
<ngx-file-drop
dropZoneLabel="Drop documents here"
(onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)"
(onFileLeave)="fileLeave($event)"
dropZoneClassName="bg-light mt-4 card">
</ngx-file-drop>
</form>
<h5 class="mt-3">Document conumser status</h5>
<p>This is what it might look like in the future.</p>
<div class="card bg-light mb-2">
<div class="card-body">
<p class="card-text"><strong>Filename.pdf:</strong> Running tesseract on page 4/8...</p>
<p><ngb-progressbar type="info" [value]="50"></ngb-progressbar></p>
</div>
</div>
<div class="card bg-light mb-2">
<div class="card-body">
<p class="card-text"><strong>Filename2.pdf:</strong> Completed.</p>
<p><ngb-progressbar type="success" [value]="100"></ngb-progressbar></p>
</div>
</div>
<app-statistics-widget></app-statistics-widget>
<app-file-upload-widget></app-file-upload-widget>
<app-consumer-status-widget></app-consumer-status-widget>
</div>
</div>

View File

@@ -4,14 +4,9 @@ import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
import { Observable } from 'rxjs';
import { DocumentService } from 'src/app/services/rest/document.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
import { environment } from 'src/environments/environment';
export interface Statistics {
documents_total?: number
documents_inbox?: number
}
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
@@ -19,53 +14,9 @@ export interface Statistics {
})
export class DashboardComponent implements OnInit {
constructor(private documentService: DocumentService, private toastService: ToastService,
public savedViewConfigService: SavedViewConfigService, private http: HttpClient) { }
savedDashboardViews = []
statistics: Statistics = {}
constructor(public savedViewConfigService: SavedViewConfigService) { }
ngOnInit(): void {
this.savedViewConfigService.getDashboardConfigs().forEach(config => {
this.documentService.list(1,10,config.sortField,config.sortDirection,config.filterRules).subscribe(result => {
this.savedDashboardViews.push({viewConfig: config, documents: result.results})
})
})
this.getStatistics().subscribe(statistics => {
this.statistics = statistics
})
}
getStatistics(): Observable<Statistics> {
return this.http.get(`${environment.apiBaseUrl}statistics/`)
}
public fileOver(event){
console.log(event);
}
public fileLeave(event){
console.log(event);
}
public dropped(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
console.log(fileEntry)
fileEntry.file((file: File) => {
console.log(file)
const formData = new FormData()
formData.append('document', file, file.name)
this.documentService.uploadDocument(formData).subscribe(result => {
this.toastService.showToast(Toast.make("Information", "The document has been uploaded and will be processed by the consumer shortly."))
}, error => {
this.toastService.showToast(Toast.makeError("An error has occured while uploading the document. Sorry!"))
})
});
}
}
}
}

View File

@@ -0,0 +1,10 @@
<h4 class="mt-3">Document consumer status</h4>
<div class="p-2 mb-1 border bg-light" *ngFor="let s of getStatus()">
<div class="mb-1"><strong>{{s.filename}}:</strong> {{s.message}}</div>
<ngb-progressbar [type]="getType(s.status)" [value]="s.current_progress" [max]="s.max_progress" class="mb-2"></ngb-progressbar>
<div *ngIf="isFinished(s)">
<button *ngIf="s.document_id" class="btn btn-sm btn-outline-secondary mr-2" routerLink="/documents/{{s.document_id}}" (click)="dismiss(s)">Open document</button>
<button class="btn btn-sm btn-outline-secondary" (click)="dismiss(s)">Dismiss</button>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConsumerStatusWidgetComponent } from './consumer-status-widget.component';
describe('ConsumerStatusWidgetComponent', () => {
let component: ConsumerStatusWidgetComponent;
let fixture: ComponentFixture<ConsumerStatusWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ConsumerStatusWidgetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ConsumerStatusWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { Component, OnInit } from '@angular/core';
import { ConsumerStatusService, FileStatus } from 'src/app/services/consumer-status.service';
@Component({
selector: 'app-consumer-status-widget',
templateUrl: './consumer-status-widget.component.html',
styleUrls: ['./consumer-status-widget.component.css']
})
export class ConsumerStatusWidgetComponent implements OnInit {
constructor(private consumerStatusService: ConsumerStatusService) { }
ngOnInit(): void {
}
getStatus() {
return this.consumerStatusService.consumerStatus
}
isFinished(status: FileStatus) {
return status.status == "FAILED" || status.status == "SUCCESS"
}
getType(status) {
switch (status) {
case "WORKING": return "primary"
case "FAILED": return "danger"
case "SUCCESS": return "success"
}
}
dismiss(status: FileStatus) {
this.consumerStatusService.dismiss(status)
}
}

View File

@@ -0,0 +1,11 @@
<h4>Upload new Document</h4>
<form>
<ngx-file-drop
dropZoneLabel="Drop documents here"
(onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)"
(onFileLeave)="fileLeave($event)"
dropZoneClassName="bg-light mt-4 card">
</ngx-file-drop>
</form>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FileUploadWidgetComponent } from './file-upload-widget.component';
describe('FileUploadWidgetComponent', () => {
let component: FileUploadWidgetComponent;
let fixture: ComponentFixture<FileUploadWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ FileUploadWidgetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(FileUploadWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,44 @@
import { Component, OnInit } from '@angular/core';
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
import { DocumentService } from 'src/app/services/rest/document.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-file-upload-widget',
templateUrl: './file-upload-widget.component.html',
styleUrls: ['./file-upload-widget.component.css']
})
export class FileUploadWidgetComponent implements OnInit {
constructor(private documentService: DocumentService, private toastService: ToastService) { }
ngOnInit(): void {
}
public fileOver(event){
console.log(event);
}
public fileLeave(event){
console.log(event);
}
public dropped(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
console.log(fileEntry)
fileEntry.file((file: File) => {
console.log(file)
const formData = new FormData()
formData.append('document', file, file.name)
this.documentService.uploadDocument(formData).subscribe(result => {
this.toastService.showInfo("The document has been uploaded and will be processed by the consumer shortly.")
}, error => {
this.toastService.showError("An error has occured while uploading the document. Sorry!")
})
});
}
}
}
}

View File

@@ -0,0 +1,16 @@
<h4>{{viewConfig.title}}</h4>
<table class="table table-sm table-hover table-borderless">
<thead>
<tr>
<th>Date created</th>
<th scope="col">Document</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let doc of documents" routerLink="/documents/{{doc.id}}">
<td>{{doc.created | date}}</td>
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags" class="ml-1"></app-tag>
</tr>
</tbody>
</table>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SavedViewWidgetComponent } from './saved-view-widget.component';
describe('SavedViewWidgetComponent', () => {
let component: SavedViewWidgetComponent;
let fixture: ComponentFixture<SavedViewWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SavedViewWidgetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SavedViewWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,41 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { ConsumerStatusService } from 'src/app/services/consumer-status.service';
import { DocumentService } from 'src/app/services/rest/document.service';
@Component({
selector: 'app-saved-view-widget',
templateUrl: './saved-view-widget.component.html',
styleUrls: ['./saved-view-widget.component.css']
})
export class SavedViewWidgetComponent implements OnInit, OnDestroy {
constructor(private documentService: DocumentService, private consumerStatusService: ConsumerStatusService) { }
@Input()
viewConfig: SavedViewConfig
documents: PaperlessDocument[]
subscription: Subscription
ngOnInit(): void {
this.reload()
this.subscription = this.consumerStatusService.onDocumentConsumptionFinished().subscribe(status => {
this.reload()
})
}
ngOnDestroy(): void {
this.subscription.unsubscribe()
}
reload() {
this.documentService.list(1,10,this.viewConfig.sortField,this.viewConfig.sortDirection,this.viewConfig.filterRules).subscribe(result => {
this.documents = result.results
})
}
}

View File

@@ -0,0 +1,3 @@
<h4>Statistics</h4>
<p>Documents in inbox: {{statistics.documents_inbox}}</p>
<p>Total documents: {{statistics.documents_total}}</p>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StatisticsWidgetComponent } from './statistics-widget.component';
describe('StatisticsWidgetComponent', () => {
let component: StatisticsWidgetComponent;
let fixture: ComponentFixture<StatisticsWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ StatisticsWidgetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(StatisticsWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,32 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
export interface Statistics {
documents_total?: number
documents_inbox?: number
}
@Component({
selector: 'app-statistics-widget',
templateUrl: './statistics-widget.component.html',
styleUrls: ['./statistics-widget.component.css']
})
export class StatisticsWidgetComponent implements OnInit {
constructor(private http: HttpClient) { }
statistics: Statistics = {}
ngOnInit(): void {
this.getStatistics().subscribe(statistics => {
this.statistics = statistics
})
}
getStatistics(): Observable<Statistics> {
return this.http.get(`${environment.apiBaseUrl}statistics/`)
}
}

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-login',
@@ -26,7 +26,7 @@ export class LoginComponent implements OnInit {
this.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => {
this.router.navigate([''])
}, (error) => {
this.toastService.showToast(Toast.makeError("Unable to log in with provided credentials."))
this.toastService.showError("Unable to log in with provided credentials.")
}
)
}