Merge branch 'dev' into fix/issue-65

This commit is contained in:
Michael Shamoon
2020-12-11 14:29:26 -08:00
committed by GitHub
84 changed files with 1406 additions and 465 deletions

View File

@@ -45,6 +45,10 @@ import { StatisticsWidgetComponent } from './components/dashboard/widgets/statis
import { UploadFileWidgetComponent } from './components/dashboard/widgets/upload-file-widget/upload-file-widget.component';
import { WidgetFrameComponent } from './components/dashboard/widgets/widget-frame/widget-frame.component';
import { PdfViewerModule } from 'ng2-pdf-viewer';
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
import { YesNoPipe } from './pipes/yes-no.pipe';
import { FileSizePipe } from './pipes/file-size.pipe';
import { DocumentTitlePipe } from './pipes/document-title.pipe';
@NgModule({
declarations: [
@@ -81,7 +85,11 @@ import { PdfViewerModule } from 'ng2-pdf-viewer';
SavedViewWidgetComponent,
StatisticsWidgetComponent,
UploadFileWidgetComponent,
WidgetFrameComponent
WidgetFrameComponent,
WelcomeWidgetComponent,
YesNoPipe,
FileSizePipe,
DocumentTitlePipe
],
imports: [
BrowserModule,

View File

@@ -3,11 +3,10 @@
<label for="created_date">{{titleDate}}</label>
<input type="date" class="form-control" id="created_date" [(ngModel)]="dateValue" (change)="dateOrTimeChanged()">
</div>
<div class="form-group col">
<div class="form-group col" *ngIf="titleTime">
<label for="created_time">{{titleTime}}</label>
<input type="time" class="form-control" id="created_time" [(ngModel)]="timeValue" (change)="dateOrTimeChanged()">
</div>
</div>

View File

@@ -40,7 +40,7 @@ export class DateTimeComponent implements OnInit,ControlValueAccessor {
titleDate: string = "Date"
@Input()
titleTime: string = "Time"
titleTime: string
@Input()
disabled: boolean = false

View File

@@ -8,7 +8,7 @@
<div class="input-group-append" ngbDropdown placement="top-right">
<button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button>
<div ngbDropdownMenu class="scrollable-menu">
<div ngbDropdownMenu class="scrollable-menu shadow">
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag.id)">
<app-tag [tag]="tag"></app-tag>
</button>

View File

@@ -1,6 +1,5 @@
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid';
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';
@Component({

View File

@@ -4,11 +4,7 @@
<div class='row'>
<div class="col-lg-8">
<app-widget-frame title="Saved views" *ngIf="savedViews.length == 0">
<p class="card-text">This space is reserved to display your saved views. Go to your documents and save a view
to have it displayed
here!</p>
</app-widget-frame>
<app-welcome-widget *ngIf="savedViews.length == 0"></app-welcome-widget>
<ng-container *ngFor="let v of savedViews">
<app-saved-view-widget [savedView]="v"></app-saved-view-widget>
@@ -22,4 +18,4 @@
<app-upload-file-widget></app-upload-file-widget>
</div>
</div>
</div>

View File

@@ -1,5 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
@Component({
@@ -10,13 +12,15 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi
export class DashboardComponent implements OnInit {
constructor(
public savedViewConfigService: SavedViewConfigService) { }
public savedViewConfigService: SavedViewConfigService,
private titleService: Title) { }
savedViews = []
ngOnInit(): void {
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
this.titleService.setTitle(`Dashboard - ${environment.appTitle}`)
}
}

View File

@@ -29,8 +29,12 @@ export class SavedViewWidgetComponent implements OnInit {
}
showAll() {
this.list.load(this.savedView)
this.router.navigate(["documents"])
if (this.savedView.showInSideBar) {
this.router.navigate(['view', this.savedView.id])
} else {
this.list.load(this.savedView)
this.router.navigate(["documents"])
}
}
}

View File

@@ -1,15 +1,18 @@
<app-widget-frame title="Upload new documents">
<form content>
<ngx-file-drop
dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)"
dropZoneClassName="bg-light card"
multiple="true"
contentClassName="justify-content-center d-flex align-items-center p-5"
[showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
<div content>
<form>
<ngx-file-drop dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" dropZoneClassName="bg-light card"
multiple="true" contentClassName="justify-content-center d-flex align-items-center p-5" [showBrowseBtn]=true
browseBtnClassName="btn btn-sm btn-outline-primary ml-2">
</ngx-file-drop>
</form>
</ngx-file-drop>
</form>
<div *ngIf="uploadVisible" class="mt-3">
<p>Uploading {{uploadStatus.length}} file(s)</p>
<ngb-progressbar [value]="loadedSum" [max]="totalSum" [striped]="true" [animated]="uploadStatus.length > 0">
</ngb-progressbar>
</div>
</div>
</app-widget-frame>

View File

@@ -1,8 +1,15 @@
import { HttpEventType } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { FileSystemFileEntry, NgxFileDropEntry } from 'ngx-file-drop';
import { DocumentService } from 'src/app/services/rest/document.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
interface UploadStatus {
loaded: number
total: number
}
@Component({
selector: 'app-upload-file-widget',
templateUrl: './upload-file-widget.component.html',
@@ -16,26 +23,59 @@ export class UploadFileWidgetComponent implements OnInit {
}
public fileOver(event){
console.log(event);
}
public fileLeave(event){
console.log(event);
}
uploadStatus: UploadStatus[] = []
completedFiles = 0
uploadVisible = false
get loadedSum() {
return this.uploadStatus.map(s => s.loaded).reduce((a,b) => a+b, this.completedFiles > 0 ? 1 : 0)
}
get totalSum() {
return this.uploadStatus.map(s => s.total).reduce((a,b) => a+b, 1)
}
public dropped(files: NgxFileDropEntry[]) {
for (const droppedFile of files) {
if (droppedFile.fileEntry.isFile) {
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
console.log(fileEntry)
let uploadStatusObject: UploadStatus = {loaded: 0, total: 1}
this.uploadStatus.push(uploadStatusObject)
this.uploadVisible = true
const fileEntry = droppedFile.fileEntry as FileSystemFileEntry;
fileEntry.file((file: File) => {
console.log(file)
const formData = new FormData()
let 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."))
this.documentService.uploadDocument(formData).subscribe(event => {
if (event.type == HttpEventType.UploadProgress) {
uploadStatusObject.loaded = event.loaded
uploadStatusObject.total = event.total
} else if (event.type == HttpEventType.Response) {
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
this.completedFiles += 1
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!"))
this.uploadStatus.splice(this.uploadStatus.indexOf(uploadStatusObject), 1)
this.completedFiles += 1
switch (error.status) {
case 400: {
this.toastService.showToast(Toast.makeError(`There was an error while uploading the document: ${error.error.document}`))
break;
}
default: {
this.toastService.showToast(Toast.makeError("An error has occurred while uploading the document. Sorry!"))
break;
}
}
})
});
}

View File

@@ -0,0 +1,16 @@
<app-widget-frame title="First steps">
<ng-container content>
<img src="assets/save-filter.png" class="float-right">
<p>Paperless is running! :)</p>
<p>You can start uploading documents by dropping them in the file upload box to the right or by dropping them in the configured consumption folder and they'll start showing up in the documents list.
After you've added some metadata to your documents, use the filtering mechanisms of paperless to create custom views (such as 'Recently added', 'Tagged TODO') and have them displayed on the dashboard instead of this message.</p>
<p>Paperless offers some more features that try to make your life easier, such as:</p>
<ul>
<li>Once you've got a couple documents in paperless and added metadata to them, paperless can assign that metadata to new documents automatically.</li>
<li>You can configure paperless to read your mails and add documents from attached files.</li>
</ul>
<p>Consult the documentation on how to use these features. The section on basic usage also has some information on how to use paperless in general.</p>
</ng-container>
</app-widget-frame>

View File

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

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-welcome-widget',
templateUrl: './welcome-widget.component.html',
styleUrls: ['./welcome-widget.component.scss']
})
export class WelcomeWidgetComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -1,4 +1,4 @@
<div class="card mb-3 shadow">
<div class="card mb-3 shadow-sm">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{title}}</h5>

View File

@@ -15,14 +15,14 @@
<span class="d-none d-lg-inline"> Download</span>
</a>
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.paperless__has_archive_version">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
</div>
<div class="btn-group" ngbDropdown role="group" *ngIf="metadata?.has_archive_version">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<a ngbDropdownItem [href]="downloadOriginalUrl">Download original</a>
</div>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
@@ -36,31 +36,137 @@
<div class="row">
<div class="col-xl">
<form [formGroup]='documentForm' (ngSubmit)="save()">
<app-input-text title="Title" formControlName="title"></app-input-text>
<ul ngbNav #nav="ngbNav" class="nav-tabs">
<li [ngbNavItem]="1">
<a ngbNavLink>Details</a>
<ng-template ngbNavContent>
<div class="form-group">
<label for="archive_serial_number">Archive Serial Number</label>
<input type="number" class="form-control" id="archive_serial_number"
formControlName='archive_serial_number'>
</div>
<app-input-text title="Title" formControlName="title"></app-input-text>
<div class="form-group">
<label for="archive_serial_number">Archive Serial Number</label>
<input type="number" class="form-control" id="archive_serial_number"
formControlName='archive_serial_number'>
</div>
<app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent"
allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type"
allowNull="true" (createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
<app-input-date-time title="Date created" titleTime="Time created" formControlName="created"></app-input-date-time>
</ng-template>
</li>
<div class="form-group">
<label for="content">Content</label>
<textarea class="form-control" id="content" rows="5" formControlName='content'></textarea>
</div>
<li [ngbNavItem]="2">
<a ngbNavLink>Content</a>
<ng-template ngbNavContent>
<div class="form-group">
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
</div>
</ng-template>
</li>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent" allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
<li [ngbNavItem]="3">
<a ngbNavLink>Metadata</a>
<ng-template ngbNavContent>
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type" allowNull="true" (createNew)="createDocumentType()"></app-input-select>
<table class="table table-borderless">
<tbody>
<tr>
<td>Date modified</td>
<td>{{document.modified | date:'medium'}}</td>
</tr>
<tr>
<td>Date added</td>
<td>{{document.added | date:'medium'}}</td>
</tr>
<tr>
<td>Media filename</td>
<td>{{metadata?.media_filename}}</td>
</tr>
<tr>
<td>Original MD5 Checksum</td>
<td>{{metadata?.original_checksum}}</td>
</tr>
<tr>
<td>Original file size</td>
<td>{{metadata?.original_size | fileSize}}</td>
</tr>
<tr>
<td>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive MD5 Checksum</td>
<td>{{metadata?.archive_checksum}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
</tr>
</tbody>
</table>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
<h6 *ngIf="metadata?.original_metadata.length > 0">
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expandOriginalMetadata = !expandOriginalMetadata" aria-controls="collapseExample">
<svg class="buttonicon" fill="currentColor" *ngIf="!expandOriginalMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expandOriginalMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
Original document metadata
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandOriginalMetadata">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata?.original_metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td>{{m.value}}</td>
</tr>
</tbody>
</table>
</div>
<h6 *ngIf="metadata?.has_archive_version && metadata?.archive_metadata.length > 0">
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expandArchivedMetadata = !expandArchivedMetadata" aria-controls="collapseExample">
<svg class="buttonicon" fill="currentColor" *ngIf="!expandArchivedMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expandArchivedMetadata">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
Archived document metadata
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expandArchivedMetadata">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata?.archive_metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td>{{m.value}}</td>
</tr>
</tbody>
</table>
</div>
</ng-template>
</li>
</ul>
<div [ngbNavOutlet]="nav" class="mt-2"></div>
<button type="button" class="btn btn-outline-secondary" (click)="discard()">Discard</button>&nbsp;
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit next</button>&nbsp;
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()">Save & edit
next</button>&nbsp;
<button type="submit" class="btn btn-primary">Save</button>&nbsp;
</form>
</div>
@@ -70,4 +176,4 @@
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="false"></pdf-viewer>
</div>
</div>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
@@ -11,6 +12,7 @@ import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentService } from 'src/app/services/rest/document.service';
import { environment } from 'src/environments/environment';
import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component';
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
@@ -22,6 +24,9 @@ import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/do
})
export class DocumentDetailComponent implements OnInit {
public expandOriginalMetadata = false;
public expandArchivedMetadata = false;
documentId: number
document: PaperlessDocument
metadata: PaperlessDocumentMetadata
@@ -51,7 +56,8 @@ export class DocumentDetailComponent implements OnInit {
private router: Router,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService) { }
private documentListViewService: DocumentListViewService,
private titleService: Title) { }
ngOnInit(): void {
this.documentForm.valueChanges.subscribe(wow => {
@@ -80,6 +86,7 @@ export class DocumentDetailComponent implements OnInit {
updateComponent(doc: PaperlessDocument) {
this.document = doc
this.titleService.setTitle(`${doc.title} - ${environment.appTitle}`)
this.documentsService.getMetadata(doc.id).subscribe(result => {
this.metadata = result
})

View File

@@ -12,7 +12,7 @@
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
</ng-container>
{{document.title}}
{{document.title | documentTitle}}
<app-tag [tag]="t" linkTitle="Filter by tag" *ngFor="let t of document.tags$ | async" class="ml-1" (click)="clickTag.emit(t.id)" [clickable]="clickTag.observers.length"></app-tag>
</h5>
<h5 class="card-title" *ngIf="document.archive_serial_number">#{{document.archive_serial_number}}</h5>

View File

@@ -1,8 +1,14 @@
<div class="col p-2 h-100" style="width: 16rem;">
<div class="card h-100 shadow-sm">
<div class=" border-bottom doc-img pr-1" [ngStyle]="{'background-image': 'url(' + getThumbUrl() + ')'}">
<div class="row" *ngFor="let t of document.tags$ | async">
<app-tag style="font-size: large;" [tag]="t" class="col text-right" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<div class="border-bottom">
<img class="card-img doc-img" [src]="getThumbUrl()">
<div style="top: 0; right: 0; font-size: large" class="text-right position-absolute mr-1">
<div *ngFor="let t of getTagsLimited$() | async">
<app-tag [tag]="t" (click)="clickTag.emit(t.id)" [clickable]="true" linkTitle="Filter by tag"></app-tag>
</div>
<div *ngIf="moreTags">
<span class="badge badge-secondary">+ {{moreTags}}</span>
</div>
</div>
</div>
@@ -11,7 +17,7 @@
<ng-container *ngIf="document.correspondent">
<a [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>:
</ng-container>
{{document.title}}
{{document.title | documentTitle}}
</p>
</div>
<div class="card-footer">

View File

@@ -1,5 +1,5 @@
.doc-img {
background-size: cover;
background-position: top;
object-fit: cover;
object-position: top;
height: 200px;
}

View File

@@ -1,4 +1,5 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { map } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentService } from 'src/app/services/rest/document.service';
@@ -21,6 +22,8 @@ export class DocumentCardSmallComponent implements OnInit {
@Output()
clickCorrespondent = new EventEmitter<number>()
moreTags: number = null
ngOnInit(): void {
}
@@ -35,4 +38,18 @@ export class DocumentCardSmallComponent implements OnInit {
getPreviewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
getTagsLimited$() {
return this.document.tags$.pipe(
map(tags => {
if (tags.length > 7) {
this.moreTags = tags.length - 6
return tags.slice(0, 6)
} else {
return tags
}
})
)
}
}

View File

@@ -24,7 +24,7 @@
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection">
<div ngbDropdown class="btn-group">
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
[class.active]="list.sortField == f.field">{{f.name}}</button>
</div>
@@ -44,7 +44,7 @@
</div>
<div class="btn-group ml-2">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="showFilter=!showFilter">
<button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg>
@@ -53,7 +53,7 @@
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu>
<div class="dropdown-menu" ngbDropdownMenu class="shadow">
<ng-container *ngIf="!list.savedViewId" >
<button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
<div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
@@ -75,7 +75,7 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<p>{{list.collectionSize || 0}} document(s)</p>
<p>{{list.collectionSize || 0}} document(s) <span *ngIf="isFiltered">(filtered)</span></p>
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
</div>
@@ -85,7 +85,7 @@
</app-document-card-large>
</div>
<table class="table table-sm border shadow" *ngIf="displayMode == 'details'">
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
<thead>
<th class="d-none d-lg-table-cell">ASN</th>
<th class="d-none d-md-table-cell">Correspondent</th>
@@ -105,7 +105,7 @@
</ng-container>
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document">{{d.title}}</a>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t.id)"></app-tag>
</td>
<td class="d-none d-xl-table-cell">

View File

@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule';
@@ -8,6 +9,7 @@ import { DocumentListViewService } from 'src/app/services/document-list-view.ser
import { DOCUMENT_SORT_FIELDS } 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 { environment } from 'src/environments/environment';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
@Component({
@@ -22,13 +24,18 @@ export class DocumentListComponent implements OnInit {
public savedViewConfigService: SavedViewConfigService,
public route: ActivatedRoute,
private toastService: ToastService,
public modalService: NgbModal) { }
public modalService: NgbModal,
private titleService: Title) { }
displayMode = 'smallCards' // largeCards, smallCards, details
filterRules: FilterRule[] = []
showFilter = false
get isFiltered() {
return this.list.filterRules?.length > 0
}
getTitle() {
return this.list.savedViewTitle || "Documents"
}
@@ -50,10 +57,12 @@ export class DocumentListComponent implements OnInit {
this.list.savedView = this.savedViewConfigService.getConfig(params.get('id'))
this.filterRules = this.list.filterRules
this.showFilter = false
this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`)
} else {
this.list.savedView = null
this.filterRules = this.list.filterRules
this.showFilter = this.filterRules.length > 0
this.titleService.setTitle(`Documents - ${environment.appTitle}`)
}
this.list.clear()
this.list.reload()

View File

@@ -34,7 +34,7 @@ export class FilterEditorComponent implements OnInit {
documentTypes: PaperlessDocumentType[] = []
newRuleClicked() {
this.filterRules.push({type: this.selectedRuleType, value: null})
this.filterRules.push({type: this.selectedRuleType, value: this.selectedRuleType.default})
this.selectedRuleType = this.getRuleTypes().length > 0 ? this.getRuleTypes()[0] : null
}

View File

@@ -1,7 +1,9 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
@@ -10,14 +12,19 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
templateUrl: './correspondent-list.component.html',
styleUrls: ['./correspondent-list.component.scss']
})
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> implements OnInit {
constructor(correspondentsService: CorrespondentService,
modalService: NgbModal) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
constructor(correspondentsService: CorrespondentService, modalService: NgbModal, private titleService: Title) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
getObjectName(object: PaperlessCorrespondent) {
return `correspondent '${object.name}'`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Correspondents - ${environment.appTitle}`)
}
getObjectName(object: PaperlessCorrespondent) {
return `correspondent '${object.name}'`
}
}

View File

@@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
@@ -10,13 +12,18 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
templateUrl: './document-type-list.component.html',
styleUrls: ['./document-type-list.component.scss']
})
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> implements OnInit {
constructor(service: DocumentTypeService, modalService: NgbModal) {
constructor(service: DocumentTypeService, modalService: NgbModal, private titleService: Title) {
super(service, modalService, DocumentTypeEditDialogComponent)
}
}
getObjectName(object: PaperlessDocumentType) {
getObjectName(object: PaperlessDocumentType) {
return `document type '${object.name}'`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Document types - ${environment.appTitle}`)
}
}

View File

@@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core';
import { kMaxLength } from 'buffer';
import { Title } from '@angular/platform-browser';
import { LOG_LEVELS, LOG_LEVEL_INFO, PaperlessLog } from 'src/app/data/paperless-log';
import { LogService } from 'src/app/services/rest/log.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-logs',
@@ -10,13 +11,14 @@ import { LogService } from 'src/app/services/rest/log.service';
})
export class LogsComponent implements OnInit {
constructor(private logService: LogService) { }
constructor(private logService: LogService, private titleService: Title) { }
logs: PaperlessLog[] = []
level: number = LOG_LEVEL_INFO
ngOnInit(): void {
this.reload()
this.titleService.setTitle(`Logs - ${environment.appTitle}`)
}
reload() {

View File

@@ -46,8 +46,8 @@
<tbody>
<tr *ngFor="let config of savedViewConfigService.getConfigs()">
<td>{{ config.title }}</td>
<td>{{ config.showInDashboard }}</td>
<td>{{ config.showInSideBar }}</td>
<td>{{ config.showInDashboard | yesno }}</td>
<td>{{ config.showInSideBar | yesno }}</td>
<td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteViewConfig(config)">Delete</button></td>
</tr>
</tbody>

View File

@@ -1,9 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { GENERAL_SETTINGS } from 'src/app/data/storage-keys';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-settings',
@@ -18,10 +20,12 @@ export class SettingsComponent implements OnInit {
constructor(
private savedViewConfigService: SavedViewConfigService,
private documentListViewService: DocumentListViewService
private documentListViewService: DocumentListViewService,
private titleService: Title
) { }
ngOnInit(): void {
this.titleService.setTitle(`Settings - ${environment.appTitle}`)
}
deleteViewConfig(config: SavedViewConfig) {

View File

@@ -1,8 +1,9 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentEditDialogComponent } from '../correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component';
@@ -11,11 +12,17 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.scss']
})
export class TagListComponent extends GenericListComponent<PaperlessTag> {
export class TagListComponent extends GenericListComponent<PaperlessTag> implements OnInit {
constructor(tagService: TagService, modalService: NgbModal) {
constructor(tagService: TagService, modalService: NgbModal, private titleService: Title) {
super(tagService, modalService, TagEditDialogComponent)
}
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Tags - ${environment.appTitle}`)
}
getColor(id) {
return TAG_COLOURS.find(c => c.id == id)

View File

@@ -1,7 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchHit } from 'src/app/data/search-result';
import { SearchService } from 'src/app/services/rest/search.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-search',
@@ -26,7 +28,7 @@ export class SearchComponent implements OnInit {
errorMessage: string
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { }
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private titleService: Title) { }
ngOnInit(): void {
this.route.queryParamMap.subscribe(paramMap => {
@@ -34,6 +36,7 @@ export class SearchComponent implements OnInit {
this.searching = true
this.currentPage = 1
this.loadPage()
this.titleService.setTitle(`Search: ${this.query} - ${environment.appTitle}`)
})
}

View File

@@ -16,19 +16,22 @@ export const FILTER_ADDED_AFTER = 14
export const FILTER_MODIFIED_BEFORE = 15
export const FILTER_MODIFIED_AFTER = 16
export const FILTER_DOES_NOT_HAVE_TAG = 17
export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false},
{id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false},
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false},
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
{id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false},
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true},
{id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true},
{id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false},
{id: FILTER_CREATED_AFTER, name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false},
@@ -50,4 +53,5 @@ export interface FilterRuleType {
filtervar: string
datatype: string //number, string, boolean, date
multi: boolean
default?: any
}

View File

@@ -1,11 +1,13 @@
export interface PaperlessDocumentMetadata {
paperless__checksum?: string
original_checksum?: string
paperless__mime_type?: string
archived_checksum?: string
paperless__filename?: string
original_mime_type?: string
paperless__has_archive_version?: boolean
media_filename?: string
has_archive_version?: boolean
}

View File

@@ -0,0 +1,8 @@
import { DocumentTitlePipe } from './document-title.pipe';
describe('DocumentTitlePipe', () => {
it('create an instance', () => {
const pipe = new DocumentTitlePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'documentTitle'
})
export class DocumentTitlePipe implements PipeTransform {
transform(value: string): unknown {
if (value) {
return value
} else {
return "(no title)"
}
}
}

View File

@@ -0,0 +1,8 @@
import { FileSizePipe } from './file-size.pipe';
describe('FileSizePipe', () => {
it('create an instance', () => {
const pipe = new FileSizePipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,77 @@
/**
* https://gist.github.com/JonCatmull/ecdf9441aaa37336d9ae2c7f9cb7289a
*
* @license
* Copyright (c) 2019 Jonathan Catmull.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import { Pipe, PipeTransform } from '@angular/core';
type unit = 'bytes' | 'KB' | 'MB' | 'GB' | 'TB' | 'PB';
type unitPrecisionMap = {
[u in unit]: number;
};
const defaultPrecisionMap: unitPrecisionMap = {
bytes: 0,
KB: 0,
MB: 1,
GB: 1,
TB: 2,
PB: 2
};
/*
* Convert bytes into largest possible unit.
* Takes an precision argument that can be a number or a map for each unit.
* Usage:
* bytes | fileSize:precision
* @example
* // returns 1 KB
* {{ 1500 | fileSize }}
* @example
* // returns 2.1 GB
* {{ 2100000000 | fileSize }}
* @example
* // returns 1.46 KB
* {{ 1500 | fileSize:2 }}
*/
@Pipe({ name: 'fileSize' })
export class FileSizePipe implements PipeTransform {
private readonly units: unit[] = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
transform(bytes: number = 0, precision: number | unitPrecisionMap = defaultPrecisionMap): string {
if (isNaN(parseFloat(String(bytes))) || !isFinite(bytes)) return '?';
let unitIndex = 0;
while (bytes >= 1024) {
bytes /= 1024;
unitIndex++;
}
const unit = this.units[unitIndex];
if (typeof precision === 'number') {
return `${bytes.toFixed(+precision)} ${unit}`;
}
return `${bytes.toFixed(precision[unit])} ${unit}`;
}
}

View File

@@ -0,0 +1,8 @@
import { YesNoPipe } from './yes-no.pipe';
describe('YesNoPipe', () => {
it('create an instance', () => {
const pipe = new YesNoPipe();
expect(pipe).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'yesno'
})
export class YesNoPipe implements PipeTransform {
transform(value: boolean): unknown {
return value ? "Yes" : "No"
}
}

View File

@@ -94,7 +94,7 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
}
uploadDocument(formData) {
return this.http.post(this.getResourceUrl(null, 'post_document'), formData)
return this.http.post(this.getResourceUrl(null, 'post_document'), formData, {reportProgress: true, observe: "events"})
}
getMetadata(id: number): Observable<PaperlessDocumentMetadata> {