Merge branch 'dev' into feature-websockets-status

This commit is contained in:
jonaswinkler
2020-12-06 22:53:54 +01:00
179 changed files with 5678 additions and 2460 deletions

View File

@@ -23,7 +23,7 @@ import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-di
import { DocumentTypeEditDialogComponent } from './components/manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { TagComponent } from './components/common/tag/tag.component';
import { SearchComponent } from './components/search/search.component';
import { ResultHightlightComponent } from './components/search/result-hightlight/result-hightlight.component';
import { ResultHighlightComponent } from './components/search/result-highlight/result-highlight.component';
import { PageHeaderComponent } from './components/common/page-header/page-header.component';
import { AppFrameComponent } from './components/app-frame/app-frame.component';
import { ToastsComponent } from './components/common/toasts/toasts.component';
@@ -45,6 +45,7 @@ import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-v
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component';
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 { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component';
@NgModule({
@@ -66,7 +67,7 @@ import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/co
DocumentTypeEditDialogComponent,
TagComponent,
SearchComponent,
ResultHightlightComponent,
ResultHighlightComponent,
PageHeaderComponent,
AppFrameComponent,
ToastsComponent,
@@ -83,8 +84,9 @@ import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/co
SavedViewWidgetComponent,
StatisticsWidgetComponent,
UploadFileWidgetComponent,
ConsumerStatusWidgetComponent,
WidgetFrameComponent
WidgetFrameComponent,
WelcomeWidgetComponent,
ConsumerStatusWidgetComponent
],
imports: [
BrowserModule,

View File

@@ -50,6 +50,7 @@
.sidebar .nav-link.active {
color: $primary;
font-weight: bold;
}
.sidebar .nav-link:hover .sidebaricon,

View File

@@ -90,7 +90,9 @@ export class AppFrameComponent implements OnInit, OnDestroy {
}
ngOnDestroy() {
this.openDocumentsSubscription.unsubscribe()
if (this.openDocumentsSubscription) {
this.openDocumentsSubscription.unsubscribe()
}
}
}

View File

@@ -1,2 +1,2 @@
<span *ngIf="!clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</span>
<a [routerLink]="" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a>
<a [routerLink]="" [title]="linkTitle" *ngIf="clickable" class="badge" [style.background]="getColour().value" [style.color]="getColour().textColor">{{tag.name}}</a>

View File

@@ -14,10 +14,10 @@ export class TagComponent implements OnInit {
tag: PaperlessTag
@Input()
clickable: boolean = false
linkTitle: string = ""
@Output()
click = new EventEmitter()
@Input()
clickable: boolean = false
ngOnInit(): void {
}

View File

@@ -3,19 +3,15 @@
</app-page-header>
<div class='row'>
<div class="col-lg">
<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>
<div class="col-lg-8">
<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>
</ng-container>
</div>
<div class="col-lg">
<div class="col-lg-4">
<app-statistics-widget></app-statistics-widget>

View File

@@ -1,6 +1,9 @@
<app-widget-frame [title]="savedView.title">
<table class="table table-sm table-hover table-borderless">
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>
<table content class="table table-sm table-hover table-borderless">
<thead>
<tr>
<th>Created</th>
@@ -10,7 +13,7 @@
<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>
<td>{{doc.title}}<app-tag [tag]="t" *ngFor="let t of doc.tags$ | async" class="ml-1"></app-tag>
</tr>
</tbody>
</table>

View File

@@ -1,7 +1,9 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { ConsumerStatusService } from 'src/app/services/consumer-status.service';
import { DocumentService } from 'src/app/services/rest/document.service';
@@ -12,7 +14,11 @@ import { DocumentService } from 'src/app/services/rest/document.service';
})
export class SavedViewWidgetComponent implements OnInit {
constructor(private documentService: DocumentService, private consumerStatusService: ConsumerStatusService) { }
constructor(
private documentService: DocumentService,
private router: Router,
private list: DocumentListViewService,
private consumerStatusService: ConsumerStatusService) { }
@Input()
savedView: SavedViewConfig
@@ -38,4 +44,9 @@ export class SavedViewWidgetComponent implements OnInit {
})
}
showAll() {
this.list.load(this.savedView)
this.router.navigate(["documents"])
}
}

View File

@@ -1,4 +1,6 @@
<app-widget-frame title="Statistics">
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
<ng-container content>
<p class="card-text">Documents in inbox: {{statistics.documents_inbox}}</p>
<p class="card-text">Total documents: {{statistics.documents_total}}</p>
</ng-container>
</app-widget-frame>

View File

@@ -1,6 +1,6 @@
<app-widget-frame title="Upload new documents">
<form>
<form content>
<ngx-file-drop
dropZoneLabel="Drop documents here or" (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)"

View File

@@ -16,26 +16,31 @@ export class UploadFileWidgetComponent implements OnInit {
}
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.")
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!")
switch (error.status) {
case 400: {
this.toastService.showError(`There was an error while uploading the document: ${error.error.document}`)
break;
}
default: {
this.toastService.showError("An error has occured 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

@@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ResultHightlightComponent } from './result-hightlight.component';
import { WelcomeWidgetComponent } from './welcome-widget.component';
describe('ResultHightlightComponent', () => {
let component: ResultHightlightComponent;
let fixture: ComponentFixture<ResultHightlightComponent>;
describe('WelcomeWidgetComponent', () => {
let component: WelcomeWidgetComponent;
let fixture: ComponentFixture<WelcomeWidgetComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ResultHightlightComponent ]
declarations: [ WelcomeWidgetComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ResultHightlightComponent);
fixture = TestBed.createComponent(WelcomeWidgetComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

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,8 +1,12 @@
<div class="card mb-3 shadow">
<div class="card-header">
<h5 class="card-title mb-0">{{title}}</h5>
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">{{title}}</h5>
<ng-content select ="[header-buttons]"></ng-content>
</div>
</div>
<div class="card-body text-dark">
<ng-content></ng-content>
<ng-content select ="[content]"></ng-content>
</div>
</div>

View File

@@ -5,12 +5,26 @@
</svg>
<span class="d-none d-lg-inline"> Delete</span>
</button>
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary mr-2">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>
<span class="d-none d-lg-inline"> Download</span>
</a>
<div class="btn-group mr-2">
<a [href]="downloadUrl" class="btn btn-sm btn-outline-primary">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>
<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>
</div>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" />
@@ -39,11 +53,11 @@
<textarea class="form-control" id="content" rows="5" formControlName='content'></textarea>
</div>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent_id" allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
<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_id" allowNull="true" (createNew)="createDocumentType()"></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_id" title="Tags"></app-input-tags>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
<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;

View File

@@ -1,22 +1,19 @@
import { DatePipe, formatDate } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
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 { TagService } from 'src/app/services/rest/tag.service';
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';
import { TagEditDialogComponent } from '../manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
@Component({
selector: 'app-document-detail',
@@ -27,9 +24,11 @@ export class DocumentDetailComponent implements OnInit {
documentId: number
document: PaperlessDocument
metadata: PaperlessDocumentMetadata
title: string
previewUrl: string
downloadUrl: string
downloadOriginalUrl: string
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
@@ -38,10 +37,10 @@ export class DocumentDetailComponent implements OnInit {
title: new FormControl(''),
content: new FormControl(''),
created: new FormControl(),
correspondent_id: new FormControl(),
document_type_id: new FormControl(),
correspondent: new FormControl(),
document_type: new FormControl(),
archive_serial_number: new FormControl(),
tags_id: new FormControl([])
tags: new FormControl([])
})
constructor(
@@ -66,6 +65,7 @@ export class DocumentDetailComponent implements OnInit {
this.documentId = +paramMap.get('id')
this.previewUrl = this.documentsService.getPreviewUrl(this.documentId)
this.downloadUrl = this.documentsService.getDownloadUrl(this.documentId)
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(this.documentId, true)
if (this.openDocumentService.getOpenDocument(this.documentId)) {
this.updateComponent(this.openDocumentService.getOpenDocument(this.documentId))
} else {
@@ -80,6 +80,9 @@ export class DocumentDetailComponent implements OnInit {
updateComponent(doc: PaperlessDocument) {
this.document = doc
this.documentsService.getMetadata(doc.id).subscribe(result => {
this.metadata = result
})
this.title = doc.title
this.documentForm.patchValue(doc)
}
@@ -90,7 +93,7 @@ export class DocumentDetailComponent implements OnInit {
modal.componentInstance.success.subscribe(newDocumentType => {
this.documentTypeService.listAll().subscribe(documentTypes => {
this.documentTypes = documentTypes.results
this.documentForm.get('document_type_id').setValue(newDocumentType.id)
this.documentForm.get('document_type').setValue(newDocumentType.id)
})
})
}
@@ -101,7 +104,7 @@ export class DocumentDetailComponent implements OnInit {
modal.componentInstance.success.subscribe(newCorrespondent => {
this.correspondentService.listAll().subscribe(correspondents => {
this.correspondents = correspondents.results
this.documentForm.get('correspondent_id').setValue(newCorrespondent.id)
this.documentForm.get('correspondent').setValue(newCorrespondent.id)
})
})
}
@@ -133,8 +136,8 @@ export class DocumentDetailComponent implements OnInit {
close() {
this.openDocumentService.closeDocument(this.document)
if (this.documentListViewService.viewId) {
this.router.navigate(['view', this.documentListViewService.viewId])
if (this.documentListViewService.savedViewId) {
this.router.navigate(['view', this.documentListViewService.savedViewId])
} else {
this.router.navigate(['documents'])
}

View File

@@ -7,11 +7,18 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">{{document.correspondent ? document.correspondent.name + ': ' : ''}}{{document.title}}<app-tag [tag]="t" *ngFor="let t of document.tags" class="ml-1"></app-tag></h5>
<h5 class="card-title">
<ng-container *ngIf="document.correspondent">
<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}}
<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>
</div>
<p class="card-text">
<app-result-hightlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailsAsHighlight()"></app-result-hightlight>
<app-result-highlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailsAsHighlight()"></app-result-highlight>
<span *ngIf="getDetailsAsString()" class="result-content">{{getDetailsAsString()}}</span>
</p>
@@ -24,6 +31,13 @@
</svg>
Edit
</a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getPreviewUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg>
View
</a>
<a type="button" class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>

View File

@@ -1,6 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
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';
@Component({
@@ -18,6 +19,12 @@ export class DocumentCardLargeComponent implements OnInit {
@Input()
details: any
@Output()
clickTag = new EventEmitter<number>()
@Output()
clickCorrespondent = new EventEmitter<number>()
ngOnInit(): void {
}
@@ -41,4 +48,8 @@ export class DocumentCardLargeComponent implements OnInit {
getDownloadUrl() {
return this.documentService.getDownloadUrl(this.document.id)
}
getPreviewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
}

View File

@@ -1,27 +1,35 @@
<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">
<app-tag [tag]="t" class="col text-right"></app-tag>
<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>
</div>
<div class="card-body p-2">
<p class="card-text">
<span class="font-weight-bold">{{document.correspondent? document.correspondent.name + ': ' : ''}}</span> {{document.title}}
<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}}
</p>
</div>
<div class="card-footer">
<div class="d-flex justify-content-between align-items-center ml-n2">
<div class="btn-group">
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Edit">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
</svg>
</a>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary">
<a [href]="getPreviewUrl()" class="btn btn-sm btn-outline-secondary" title="View in browser">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-search" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.442 10.442a1 1 0 0 1 1.415 0l3.85 3.85a1 1 0 0 1-1.414 1.415l-3.85-3.85a1 1 0 0 1 0-1.415z"/>
<path fill-rule="evenodd" d="M6.5 12a5.5 5.5 0 1 0 0-11 5.5 5.5 0 0 0 0 11zM13 6.5a6.5 6.5 0 1 1-13 0 6.5 6.5 0 0 1 13 0z"/>
</svg>
</a>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-download" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/>
<path fill-rule="evenodd" d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/>

View File

@@ -1,5 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
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';
@Component({
@@ -14,6 +15,12 @@ export class DocumentCardSmallComponent implements OnInit {
@Input()
document: PaperlessDocument
@Output()
clickTag = new EventEmitter<number>()
@Output()
clickCorrespondent = new EventEmitter<number>()
ngOnInit(): void {
}
@@ -24,4 +31,8 @@ export class DocumentCardSmallComponent implements OnInit {
getDownloadUrl() {
return this.documentService.getDownloadUrl(this.document.id)
}
getPreviewUrl() {
return this.documentService.getPreviewUrl(this.document.id)
}
}

View File

@@ -21,13 +21,12 @@
</svg>
</label>
</div>
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="docs.sortDirection"
*ngIf="!docs.viewId">
<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">
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSort(f.field)"
[class.active]="docs.sortField == f.field">{{f.name}}</button>
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="list.sortField = f.field"
[class.active]="list.sortField == f.field">{{f.name}}</button>
</div>
</div>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
@@ -43,7 +42,7 @@
</svg>
</label>
</div>
<div class="btn-group ml-2" *ngIf="!docs.viewId">
<div class="btn-group ml-2">
<button type="button" class="btn btn-sm btn-outline-primary" (click)="showFilter=!showFilter">
<svg class="toolbaricon" fill="currentColor">
@@ -55,9 +54,13 @@
<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>
<button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
<div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
<button ngbDropdownItem (click)="saveViewConfig()">Save current view</button>
<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>
</ng-container>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
</div>
</div>
@@ -67,21 +70,22 @@
<div class="card w-100 mb-3" [hidden]="!showFilter">
<div class="card-body">
<h5 class="card-title">Filter</h5>
<app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()"></app-filter-editor>
<app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor>
</div>
</div>
<div class="row m-0 justify-content-end">
<ngb-pagination [pageSize]="docs.currentPageSize" [collectionSize]="docs.collectionSize" [(page)]="docs.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="reload()" aria-label="Default pagination"></ngb-pagination>
<div class="d-flex justify-content-between align-items-center">
<p>{{list.collectionSize || 0}} document(s)</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>
<div *ngIf="displayMode == 'largeCards'">
<app-document-card-large *ngFor="let d of docs.documents" [document]="d" [details]="d.content">
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)">
</app-document-card-large>
</div>
<table class="table table-hover table-sm border shadow" *ngIf="displayMode == 'details'">
<table class="table table-sm border shadow" *ngIf="displayMode == 'details'">
<thead>
<th class="d-none d-lg-table-cell">ASN</th>
<th class="d-none d-md-table-cell">Correspondent</th>
@@ -91,20 +95,35 @@
<th class="d-none d-xl-table-cell">Added</th>
</thead>
<tbody>
<tr *ngFor="let d of docs.documents" routerLink="/documents/{{d.id}}">
<td class="d-none d-lg-table-cell">{{d.archive_serial_number}}</td>
<td class="d-none d-md-table-cell">{{d.correspondent ? d.correspondent.name : ''}}</td>
<td>{{d.title}}<app-tag [tag]="t" *ngFor="let t of d.tags" class="ml-1"></app-tag></td>
<td class="d-none d-xl-table-cell">{{d.document_type ? d.document_type.name : ''}}</td>
<td>{{d.created | date}}</td>
<td class="d-none d-xl-table-cell">{{d.added | date}}</td>
<tr *ngFor="let d of list.documents">
<td class="d-none d-lg-table-cell">
{{d.archive_serial_number}}
</td>
<td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent">
<a [routerLink]="" (click)="filterByCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
</ng-container>
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document">{{d.title}}</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">
<ng-container *ngIf="d.document_type">
<a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
</ng-container>
</td>
<td>
{{d.created | date}}
</td>
<td class="d-none d-xl-table-cell">
{{d.added | date}}
</td>
</tr>
</tbody>
</table>
<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
<app-document-card-small [document]="d" *ngFor="let d of docs.documents"></app-document-card-small>
<app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>
</div>
<p *ngIf="docs.documents.length == 0" class="mx-auto">No results</p>

View File

@@ -1,11 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule';
import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
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 { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
@Component({
@@ -16,9 +18,10 @@ import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-vi
export class DocumentListComponent implements OnInit {
constructor(
public docs: DocumentListViewService,
public list: DocumentListViewService,
public savedViewConfigService: SavedViewConfigService,
public route: ActivatedRoute,
private toastService: ToastService,
public modalService: NgbModal) { }
displayMode = 'smallCards' // largeCards, smallCards, details
@@ -27,17 +30,13 @@ export class DocumentListComponent implements OnInit {
showFilter = false
getTitle() {
return this.docs.viewConfigOverride ? this.docs.viewConfigOverride.title : "Documents"
return this.list.savedViewTitle || "Documents"
}
getSortFields() {
return DOCUMENT_SORT_FIELDS
}
setSort(field: string) {
this.docs.sortField = field
}
saveDisplayMode() {
localStorage.setItem('document-list:displayMode', this.displayMode)
}
@@ -48,41 +47,90 @@ export class DocumentListComponent implements OnInit {
}
this.route.paramMap.subscribe(params => {
if (params.has('id')) {
this.docs.viewConfigOverride = this.savedViewConfigService.getConfig(params.get('id'))
this.list.savedView = this.savedViewConfigService.getConfig(params.get('id'))
this.filterRules = this.list.filterRules
this.showFilter = false
} else {
this.filterRules = this.docs.filterRules
this.list.savedView = null
this.filterRules = this.list.filterRules
this.showFilter = this.filterRules.length > 0
this.docs.viewConfigOverride = null
}
this.reload()
this.list.clear()
this.list.reload()
})
}
reload() {
this.docs.reload()
applyFilterRules() {
this.list.filterRules = this.filterRules
}
applyFilterRules() {
this.docs.filterRules = this.filterRules
clearFilterRules() {
this.list.filterRules = this.filterRules
this.showFilter = false
}
loadViewConfig(config: SavedViewConfig) {
this.filterRules = cloneFilterRules(config.filterRules)
this.docs.loadViewConfig(config)
this.list.load(config)
}
saveViewConfig() {
this.savedViewConfigService.updateConfig(this.list.savedView)
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`))
}
saveViewConfigAs() {
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
modal.componentInstance.saveClicked.subscribe(formValue => {
this.savedViewConfigService.saveConfig({
this.savedViewConfigService.newConfig({
title: formValue.title,
showInDashboard: formValue.showInDashboard,
showInSideBar: formValue.showInSideBar,
filterRules: this.docs.filterRules,
sortDirection: this.docs.sortDirection,
sortField: this.docs.sortField
filterRules: this.list.filterRules,
sortDirection: this.list.sortDirection,
sortField: this.list.sortField
})
modal.close()
})
}
filterByTag(tag_id: number) {
let filterRules = this.list.filterRules
if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) {
return
}
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id})
this.filterRules = filterRules
this.applyFilterRules()
}
filterByCorrespondent(correspondent_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT)
if (existing_rule && existing_rule.value == correspondent_id) {
return
} else if (existing_rule) {
existing_rule.value = correspondent_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id})
}
this.filterRules = filterRules
this.applyFilterRules()
}
filterByDocumentType(document_type_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE)
if (existing_rule && existing_rule.value == document_type_id) {
return
} else if (existing_rule) {
existing_rule.value = document_type_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id})
}
this.filterRules = filterRules
this.applyFilterRules()
}
}

View File

@@ -18,6 +18,9 @@ export class FilterEditorComponent implements OnInit {
constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { }
@Output()
clear = new EventEmitter()
@Input()
filterRules: FilterRule[] = []
@@ -48,7 +51,7 @@ export class FilterEditorComponent implements OnInit {
clearClicked() {
this.filterRules.splice(0,this.filterRules.length)
this.apply.next()
this.clear.next()
}
ngOnInit(): void {

View File

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

View File

@@ -2,11 +2,11 @@ import { Component, Input, OnInit } from '@angular/core';
import { SearchHitHighlight } from 'src/app/data/search-result';
@Component({
selector: 'app-result-hightlight',
templateUrl: './result-hightlight.component.html',
styleUrls: ['./result-hightlight.component.scss']
selector: 'app-result-highlight',
templateUrl: './result-highlight.component.html',
styleUrls: ['./result-highlight.component.scss']
})
export class ResultHightlightComponent implements OnInit {
export class ResultHighlightComponent implements OnInit {
constructor() { }

View File

@@ -1,13 +1,21 @@
<app-page-header title="Search results">
</app-page-header>
<p>Search string: <i>{{query}}</i></p>
<div *ngIf="errorMessage" class="alert alert-danger">Invalid search query: {{errorMessage}}</div>
<div [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
<p>
Search string: <i>{{query}}</i>
<ng-container *ngIf="correctedQuery">
- Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?
</ng-container>
</p>
<div *ngIf="!errorMessage" [class.result-content-searching]="searching" infiniteScroll (scrolled)="onScroll()">
<p>{{resultCount}} result(s)</p>
<app-document-card-large *ngFor="let result of results"
[document]="result.document"
[details]="result.highlights">
</app-document-card-large>
</div>
</div>

View File

@@ -11,5 +11,5 @@
}
.result-content-searching {
opacity: 0.2;
opacity: 0.3;
}

View File

@@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchHit } from 'src/app/data/search-result';
import { SearchService } from 'src/app/services/rest/search.service';
@@ -9,7 +9,7 @@ import { SearchService } from 'src/app/services/rest/search.service';
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
results: SearchHit[] = []
query: string = ""
@@ -22,7 +22,11 @@ export class SearchComponent implements OnInit {
resultCount
constructor(private searchService: SearchService, private route: ActivatedRoute) { }
correctedQuery: string = null
errorMessage: string
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { }
ngOnInit(): void {
this.route.queryParamMap.subscribe(paramMap => {
@@ -31,10 +35,16 @@ export class SearchComponent implements OnInit {
this.currentPage = 1
this.loadPage()
})
}
searchCorrectedQuery() {
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery}})
}
loadPage(append: boolean = false) {
this.errorMessage = null
this.correctedQuery = null
this.searchService.search(this.query, this.currentPage).subscribe(result => {
if (append) {
this.results.push(...result.results)
@@ -44,12 +54,17 @@ export class SearchComponent implements OnInit {
this.pageCount = result.page_count
this.searching = false
this.resultCount = result.count
this.correctedQuery = result.corrected_query
}, error => {
this.searching = false
this.resultCount = 1
this.pageCount = 1
this.results = []
this.errorMessage = error.error
})
}
onScroll() {
console.log(this.currentPage)
console.log(this.pageCount)
if (this.currentPage < this.pageCount) {
this.currentPage += 1
this.loadPage(true)

View File

@@ -1,31 +1,51 @@
export const FILTER_TITLE = 0
export const FILTER_CONTENT = 1
export const FILTER_ASN = 2
export const FILTER_CORRESPONDENT = 3
export const FILTER_DOCUMENT_TYPE = 4
export const FILTER_IS_IN_INBOX = 5
export const FILTER_HAS_TAG = 6
export const FILTER_HAS_ANY_TAG = 7
export const FILTER_CREATED_BEFORE = 8
export const FILTER_CREATED_AFTER = 9
export const FILTER_CREATED_YEAR = 10
export const FILTER_CREATED_MONTH = 11
export const FILTER_CREATED_DAY = 12
export const FILTER_ADDED_BEFORE = 13
export const FILTER_ADDED_AFTER = 14
export const FILTER_MODIFIED_BEFORE = 15
export const FILTER_MODIFIED_AFTER = 16
export const FILTER_RULE_TYPES: FilterRuleType[] = [
{name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false},
{name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false},
{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},
{name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
{name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false},
{name: "Document type is", filtervar: "document_type__id", datatype: "document_type", 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},
{name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false},
{name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
{name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false},
{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},
{name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false},
{name: "Created after", filtervar: "created__date__gt", datatype: "date", multi: false},
{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},
{name: "Year created is", filtervar: "created__year", datatype: "number", multi: false},
{name: "Month created is", filtervar: "created__month", datatype: "number", multi: false},
{name: "Day created is", filtervar: "created__day", datatype: "number", multi: false},
{id: FILTER_CREATED_YEAR, name: "Year created is", filtervar: "created__year", datatype: "number", multi: false},
{id: FILTER_CREATED_MONTH, name: "Month created is", filtervar: "created__month", datatype: "number", multi: false},
{id: FILTER_CREATED_DAY, name: "Day created is", filtervar: "created__day", datatype: "number", multi: false},
{name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false},
{name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false},
{id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false},
{id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false},
{name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false},
{name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false},
]
export interface FilterRuleType {
id: number
name: string
filtervar: string
datatype: string //number, string, boolean, date

View File

@@ -0,0 +1,11 @@
export interface PaperlessDocumentMetadata {
paperless__checksum?: string
paperless__mime_type?: string
paperless__filename?: string
paperless__has_archive_version?: boolean
}

View File

@@ -2,16 +2,17 @@ import { PaperlessCorrespondent } from './paperless-correspondent'
import { ObjectWithId } from './object-with-id'
import { PaperlessTag } from './paperless-tag'
import { PaperlessDocumentType } from './paperless-document-type'
import { Observable } from 'rxjs'
export interface PaperlessDocument extends ObjectWithId {
correspondent?: PaperlessCorrespondent
correspondent$?: Observable<PaperlessCorrespondent>
correspondent_id?: number
correspondent?: number
document_type?: PaperlessDocumentType
document_type$?: Observable<PaperlessDocumentType>
document_type_id?: number
document_type?: number
title?: string
@@ -19,9 +20,9 @@ export interface PaperlessDocument extends ObjectWithId {
file_type?: string
tags?: PaperlessTag[]
tags$?: Observable<PaperlessTag[]>
tags_id?: number[]
tags?: number[]
checksum?: string

View File

@@ -21,7 +21,9 @@ export interface SearchResult {
page?: number
page_count?: number
corrected_query?: string
results?: SearchHit[]
}
}

View File

@@ -7,6 +7,12 @@ import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys';
import { DocumentService } from './rest/document.service';
/**
* This service manages the document list which is displayed using the document list view.
*
* This service also serves saved views by transparently switching between the document list
* and saved views on request. See below.
*/
@Injectable({
providedIn: 'root'
})
@@ -14,80 +20,133 @@ export class DocumentListViewService {
static DEFAULT_SORT_FIELD = 'created'
isReloading: boolean = false
documents: PaperlessDocument[] = []
currentPage = 1
currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
collectionSize: number
private currentViewConfig: SavedViewConfig
//TODO: make private
viewConfigOverride: SavedViewConfig
/**
* This is the current config for the document list. The service will always remember the last settings used for the document list.
*/
private _documentListViewConfig: SavedViewConfig
/**
* Optionally, this is the currently selected saved view, which might be null.
*/
private _savedViewConfig: SavedViewConfig
get viewId() {
return this.viewConfigOverride?.id
get savedView() {
return this._savedViewConfig
}
set savedView(value) {
if (value) {
//this is here so that we don't modify value, which might be the actual instance of the saved view.
this._savedViewConfig = Object.assign({}, value)
} else {
this._savedViewConfig = null
}
}
get savedViewId() {
return this.savedView?.id
}
get savedViewTitle() {
return this.savedView?.title
}
get documentListView() {
return this._documentListViewConfig
}
set documentListView(value) {
if (value) {
this._documentListViewConfig = Object.assign({}, value)
this.saveDocumentListView()
}
}
/**
* This is what switches between the saved views and the document list view. Everything on the document list uses
* this property to determine the settings for the currently displayed document list.
*/
get view() {
return this.savedView || this.documentListView
}
load(config: SavedViewConfig) {
this.view.filterRules = cloneFilterRules(config.filterRules)
this.view.sortDirection = config.sortDirection
this.view.sortField = config.sortField
this.reload()
}
clear() {
this.collectionSize = null
this.documents = []
this.currentPage = 1
}
reload(onFinish?) {
let viewConfig = this.viewConfigOverride || this.currentViewConfig
this.isReloading = true
this.documentService.list(
this.currentPage,
this.currentPageSize,
viewConfig.sortField,
viewConfig.sortDirection,
viewConfig.filterRules).subscribe(
this.view.sortField,
this.view.sortDirection,
this.view.filterRules).subscribe(
result => {
this.collectionSize = result.count
this.documents = result.results
if (onFinish) {
onFinish()
}
this.isReloading = false
},
error => {
if (error.error['detail'] == 'Invalid page.') {
this.currentPage = 1
this.reload()
}
this.isReloading = false
})
}
set filterRules(filterRules: FilterRule[]) {
this.currentViewConfig.filterRules = cloneFilterRules(filterRules)
this.saveCurrentViewConfig()
//we're going to clone the filterRules object, since we don't
//want changes in the filter editor to propagate into here right away.
this.view.filterRules = cloneFilterRules(filterRules)
this.reload()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
return cloneFilterRules(this.currentViewConfig.filterRules)
return cloneFilterRules(this.view.filterRules)
}
set sortField(field: string) {
this.currentViewConfig.sortField = field
this.saveCurrentViewConfig()
this.view.sortField = field
this.saveDocumentListView()
this.reload()
}
get sortField(): string {
return this.currentViewConfig.sortField
return this.view.sortField
}
set sortDirection(direction: string) {
this.currentViewConfig.sortDirection = direction
this.saveCurrentViewConfig()
this.view.sortDirection = direction
this.saveDocumentListView()
this.reload()
}
get sortDirection(): string {
return this.currentViewConfig.sortDirection
return this.view.sortDirection
}
loadViewConfig(config: SavedViewConfig) {
Object.assign(this.currentViewConfig, config)
this.reload()
}
private saveCurrentViewConfig() {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.currentViewConfig))
private saveDocumentListView() {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
}
getLastPage(): number {
@@ -134,21 +193,21 @@ export class DocumentListViewService {
}
constructor(private documentService: DocumentService) {
let currentViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
if (currentViewConfigJson) {
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
if (documentListViewConfigJson) {
try {
this.currentViewConfig = JSON.parse(currentViewConfigJson)
this.documentListView = JSON.parse(documentListViewConfigJson)
} catch (e) {
sessionStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
this.currentViewConfig = null
this.documentListView = null
}
}
if (!this.currentViewConfig) {
this.currentViewConfig = {
if (!this.documentListView) {
this.documentListView = {
filterRules: [],
sortDirection: 'des',
sortField: 'created'
}
}
}
}
}

View File

@@ -1,5 +1,6 @@
import { HttpClient, HttpParams } from '@angular/common/http'
import { Observable } from 'rxjs'
import { Observable, of, Subject } from 'rxjs'
import { map, publishReplay, refCount } from 'rxjs/operators'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { Results } from 'src/app/data/results'
import { environment } from 'src/environments/environment'
@@ -51,8 +52,28 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
return this.http.get<Results<T>>(this.getResourceUrl(), {params: httpParams})
}
private _listAll: Observable<Results<T>>
listAll(ordering?: string, extraParams?): Observable<Results<T>> {
return this.list(1, 100000, ordering, extraParams)
if (!this._listAll) {
this._listAll = this.list(1, 100000, ordering, extraParams).pipe(
publishReplay(1),
refCount()
)
}
return this._listAll
}
getCached(id: number): Observable<T> {
return this.listAll().pipe(
map(list => list.results.find(o => o.id == id))
)
}
getCachedMany(ids: number[]): Observable<T[]> {
return this.listAll().pipe(
map(list => ids.map(id => list.results.find(o => o.id == id)))
)
}
get(id: number): Observable<T> {
@@ -60,14 +81,17 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
}
create(o: T): Observable<T> {
this._listAll = null
return this.http.post<T>(this.getResourceUrl(), o)
}
delete(o: T): Observable<any> {
this._listAll = null
return this.http.delete(this.getResourceUrl(o.id))
}
update(o: T): Observable<T> {
this._listAll = null
return this.http.put<T>(this.getResourceUrl(o.id), o)
}
}

View File

@@ -1,10 +1,15 @@
import { Injectable } from '@angular/core';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessDocumentMetadata } from 'src/app/data/paperless-document-metadata';
import { AbstractPaperlessService } from './abstract-paperless-service';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Results } from 'src/app/data/results';
import { FilterRule } from 'src/app/data/filter-rule';
import { map } from 'rxjs/operators';
import { CorrespondentService } from './correspondent.service';
import { DocumentTypeService } from './document-type.service';
import { TagService } from './tag.service';
export const DOCUMENT_SORT_FIELDS = [
@@ -26,7 +31,7 @@ export const SORT_DIRECTION_DESCENDING = "des"
})
export class DocumentService extends AbstractPaperlessService<PaperlessDocument> {
constructor(http: HttpClient) {
constructor(http: HttpClient, private correspondentService: CorrespondentService, private documentTypeService: DocumentTypeService, private tagService: TagService) {
super(http, 'documents')
}
@@ -46,24 +51,54 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
}
}
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> {
return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules))
addObservablesToDocument(doc: PaperlessDocument) {
if (doc.correspondent) {
doc.correspondent$ = this.correspondentService.getCached(doc.correspondent)
}
if (doc.document_type) {
doc.document_type$ = this.documentTypeService.getCached(doc.document_type)
}
if (doc.tags) {
doc.tags$ = this.tagService.getCachedMany(doc.tags)
}
return doc
}
getPreviewUrl(id: number): string {
return this.getResourceUrl(id, 'preview')
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> {
return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules)).pipe(
map(results => {
results.results.forEach(doc => this.addObservablesToDocument(doc))
return results
})
)
}
getPreviewUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'preview')
if (original) {
url += "?original=true"
}
return url
}
getThumbUrl(id: number): string {
return this.getResourceUrl(id, 'thumb')
}
getDownloadUrl(id: number): string {
return this.getResourceUrl(id, 'download')
getDownloadUrl(id: number, original: boolean = false): string {
let url = this.getResourceUrl(id, 'download')
if (original) {
url += "?original=true"
}
return url
}
uploadDocument(formData) {
return this.http.post(this.getResourceUrl(null, 'post_document'), formData)
}
getMetadata(id: number): Observable<PaperlessDocumentMetadata> {
return this.http.get<PaperlessDocumentMetadata>(this.getResourceUrl(id, 'metadata'))
}
}

View File

@@ -1,9 +1,11 @@
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { SearchResult } from 'src/app/data/search-result';
import { environment } from 'src/environments/environment';
import { DocumentService } from './document.service';
@Injectable({
@@ -11,14 +13,19 @@ import { environment } from 'src/environments/environment';
})
export class SearchService {
constructor(private http: HttpClient) { }
constructor(private http: HttpClient, private documentService: DocumentService) { }
search(query: string, page?: number): Observable<SearchResult> {
let httpParams = new HttpParams().set('query', query)
if (page) {
httpParams = httpParams.set('page', page.toString())
}
return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams})
return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams}).pipe(
map(result => {
result.results.forEach(hit => this.documentService.addObservablesToDocument(hit.document))
return result
})
)
}
autocomplete(term: string): Observable<string[]> {

View File

@@ -36,13 +36,21 @@ export class SavedViewConfigService {
return this.configs.find(sf => sf.id == id)
}
saveConfig(config: SavedViewConfig) {
newConfig(config: SavedViewConfig) {
config.id = uuidv4()
this.configs.push(config)
this.save()
}
updateConfig(config: SavedViewConfig) {
let savedConfig = this.configs.find(c => c.id == config.id)
if (savedConfig) {
Object.assign(savedConfig, config)
this.save()
}
}
private save() {
localStorage.setItem('saved-view-config-service:savedConfigs', JSON.stringify(this.configs))
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB