Merge branch 'dev' into pdf-viewer-mobile-improvements

This commit is contained in:
Michael Shamoon
2022-02-14 22:00:39 -08:00
440 changed files with 101547 additions and 10573 deletions

View File

@@ -1,9 +1,18 @@
<app-page-header [(title)]="title">
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
<div class="input-group input-group-sm mr-5 d-none d-md-flex" *ngIf="getContentType() == 'application/pdf' && !useNativePdfViewer">
<div class="input-group-prepend">
<div class="input-group-text" i18n>Page</div>
</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
<div class="input-group-append">
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger mr-2 ml-auto" (click)="delete()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>
<span class="d-none d-lg-inline"> Delete</span>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Delete</span>
</button>
<div class="btn-group mr-2">
@@ -11,25 +20,28 @@
<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>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Download</span>
</a>
<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>
<a ngbDropdownItem [href]="downloadOriginalUrl" i18n>Download original</a>
</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>More like this</span>
</button>
<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" />
</svg>
<span class="d-none d-lg-inline"> Close</span>
</svg>&nbsp;<span class="d-none d-lg-inline" i18n>Close</span>
</button>
</app-page-header>
@@ -41,27 +53,23 @@
<ul ngbNav #nav="ngbNav" class="nav-tabs">
<li [ngbNavItem]="1">
<a ngbNavLink>Details</a>
<a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent>
<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-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
<app-input-date i18n-title title="Date created" formControlName="created" [error]="error?.created"></app-input-date>
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
(createNew)="createCorrespondent($event)" [suggestions]="suggestions?.correspondents"></app-input-select>
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
(createNew)="createDocumentType($event)" [suggestions]="suggestions?.document_types"></app-input-select>
<app-input-tags formControlName="tags" [suggestions]="suggestions?.tags"></app-input-tags>
</ng-template>
</li>
<li [ngbNavItem]="2">
<a ngbNavLink>Content</a>
<a ngbNavLink i18n>Content</a>
<ng-template ngbNavContent>
<div class="form-group">
<textarea class="form-control" id="content" rows="20" formControlName='content'></textarea>
@@ -70,93 +78,48 @@
</li>
<li [ngbNavItem]="3">
<a ngbNavLink>Metadata</a>
<a ngbNavLink i18n>Metadata</a>
<ng-template ngbNavContent>
<table class="table table-borderless">
<tbody>
<tr>
<td>Date modified</td>
<td>{{document.modified | date:'medium'}}</td>
<td i18n>Date modified</td>
<td>{{document.modified | customDate}}</td>
</tr>
<tr>
<td>Date added</td>
<td>{{document.added | date:'medium'}}</td>
<td i18n>Date added</td>
<td>{{document.added | customDate}}</td>
</tr>
<tr>
<td>Media filename</td>
<td i18n>Media filename</td>
<td>{{metadata?.media_filename}}</td>
</tr>
<tr>
<td>Original MD5 Checksum</td>
<td i18n>Original MD5 checksum</td>
<td>{{metadata?.original_checksum}}</td>
</tr>
<tr>
<td>Original file size</td>
<td i18n>Original file size</td>
<td>{{metadata?.original_size | fileSize}}</td>
</tr>
<tr>
<td>Original mime type</td>
<td i18n>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive MD5 Checksum</td>
<td i18n>Archive MD5 checksum</td>
<td>{{metadata?.archive_checksum}}</td>
</tr>
<tr *ngIf="metadata?.has_archive_version">
<td>Archive file size</td>
<td i18n>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
</tr>
</tbody>
</table>
<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>
<app-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata" *ngIf="metadata?.original_metadata?.length > 0"></app-metadata-collapse>
<app-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata" *ngIf="metadata?.archive_metadata?.length > 0"></app-metadata-collapse>
</ng-template>
</li>
@@ -173,16 +136,24 @@
<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="submit" class="btn btn-primary">Save</button>&nbsp;
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="networkActive">Discard</button>&nbsp;
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="networkActive">Save & next</button>&nbsp;
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>&nbsp;
</form>
</div>
<div class="col-md-6 col-xl-8 mb-3 d-none d-md-block" #pdfPreview>
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true"></pdf-viewer>
</div>
<ng-container *ngIf="getContentType() == 'application/pdf'">
<div class="preview-sticky pdf-viewer-container" *ngIf="!useNativePdfViewer ; else nativePdfViewer">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" [render-text-mode]="2" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
<ng-template #nativePdfViewer>
<object [data]="previewUrl | safe" class="preview-sticky" width="100%"></object>
</ng-template>
</ng-container>
<ng-container *ngIf="getContentType() == 'text/plain'">
<object [data]="previewUrl | safe" type="text/plain" class="preview-sticky" width="100%"></object>
</ng-container>
</div>
</div>

View File

@@ -1,6 +1,9 @@
.pdf-viewer-container {
.preview-sticky {
height: calc(100vh - 160px);
top: 70px;
position: sticky;
}
.pdf-viewer-container {
background-color: gray;
}

View File

@@ -1,21 +1,26 @@
import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNav } 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 { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe';
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 { environment } from 'src/environments/environment';
import { DeleteDialogComponent } from '../common/delete-dialog/delete-dialog.component';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-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 { PDFDocumentProxy } from 'ng2-pdf-viewer';
import { ToastService } from 'src/app/services/toast.service';
import { TextComponent } from '../common/input/text/text.component';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions';
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
@Component({
selector: 'app-document-detail',
@@ -24,12 +29,21 @@ import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/do
})
export class DocumentDetailComponent implements OnInit {
public expandOriginalMetadata = false;
public expandArchivedMetadata = false;
@ViewChild("inputTitle")
titleInput: TextComponent
expandOriginalMetadata = false
expandArchivedMetadata = false
error: any
networkActive = false
documentId: number
document: PaperlessDocument
metadata: PaperlessDocumentMetadata
suggestions: PaperlessDocumentSuggestions
title: string
previewUrl: string
downloadUrl: string
@@ -48,6 +62,9 @@ export class DocumentDetailComponent implements OnInit {
tags: new FormControl([])
})
previewCurrentPage: number = 1
previewNumPages: number = 1
@ViewChild('nav') nav: NgbNav
@ViewChild('pdfPreview') set pdfPreview(element) {
// this gets called when compontent added or removed from DOM
@@ -66,7 +83,13 @@ export class DocumentDetailComponent implements OnInit {
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService,
private titleService: Title) { }
private documentTitlePipe: DocumentTitlePipe,
private toastService: ToastService,
private settings: SettingsService) { }
get useNativePdfViewer(): boolean {
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
}
getContentType() {
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
@@ -85,6 +108,7 @@ export class DocumentDetailComponent implements OnInit {
this.previewUrl = this.documentsService.getPreviewUrl(this.documentId)
this.downloadUrl = this.documentsService.getDownloadUrl(this.documentId)
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(this.documentId, true)
this.suggestions = null
if (this.openDocumentService.getOpenDocument(this.documentId)) {
this.updateComponent(this.openDocumentService.getOpenDocument(this.documentId))
} else {
@@ -99,17 +123,24 @@ 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
}, error => {
this.metadata = null
})
this.title = doc.title
this.documentsService.getSuggestions(doc.id).subscribe(result => {
this.suggestions = result
}, error => {
this.suggestions = null
})
this.title = this.documentTitlePipe.transform(doc.title)
this.documentForm.patchValue(doc)
}
createDocumentType() {
createDocumentType(newName: string) {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
if (newName) modal.componentInstance.object = { name: newName }
modal.componentInstance.success.subscribe(newDocumentType => {
this.documentTypeService.listAll().subscribe(documentTypes => {
this.documentTypes = documentTypes.results
@@ -118,9 +149,10 @@ export class DocumentDetailComponent implements OnInit {
})
}
createCorrespondent() {
createCorrespondent(newName: string) {
var modal = this.modalService.open(CorrespondentEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
if (newName) modal.componentInstance.object = { name: newName }
modal.componentInstance.success.subscribe(newCorrespondent => {
this.correspondentService.listAll().subscribe(correspondents => {
this.correspondents = correspondents.results
@@ -138,55 +170,76 @@ export class DocumentDetailComponent implements OnInit {
}
save() {
this.networkActive = true
this.documentsService.update(this.document).subscribe(result => {
this.close()
this.networkActive = false
this.error = null
}, error => {
this.networkActive = false
this.error = error.error
})
}
saveEditNext() {
this.networkActive = true
this.documentsService.update(this.document).subscribe(result => {
this.error = null
this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
this.networkActive = false
if (nextDocId) {
this.openDocumentService.closeDocument(this.document)
this.router.navigate(['documents', nextDocId])
this.titleInput.focus()
}
}, error => {
this.networkActive = false
})
}, error => {
this.networkActive = false
this.error = error.error
})
}
close() {
this.openDocumentService.closeDocument(this.document)
if (this.documentListViewService.savedViewId) {
this.router.navigate(['view', this.documentListViewService.savedViewId])
if (this.documentListViewService.activeSavedViewId) {
this.router.navigate(['view', this.documentListViewService.activeSavedViewId])
} else {
this.router.navigate(['documents'])
}
}
delete() {
let modal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'})
modal.componentInstance.message = `Do you really want to delete document '${this.document.title}'?`
modal.componentInstance.message2 = `The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.deleteClicked.subscribe(() => {
let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
modal.componentInstance.title = $localize`Confirm delete`
modal.componentInstance.messageBold = $localize`Do you really want to delete document "${this.document.title}"?`
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document`
modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.documentsService.delete(this.document).subscribe(() => {
modal.close()
this.close()
}, error => {
this.toastService.showError($localize`Error deleting document: ${JSON.stringify(error)}`)
modal.componentInstance.buttonsEnabled = true
})
})
}
moreLike() {
this.documentListViewService.quickFilter([{rule_type: FILTER_FULLTEXT_MORELIKE, value: this.documentId.toString()}])
}
hasNext() {
return this.documentListViewService.hasNext(this.documentId)
}
previewCreated() {
console.log('Preview Created');
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
}
mobilePreviewCreated() {
console.log('Mobile Preview Created');
}
}

View File

@@ -0,0 +1,23 @@
<h6>
<button type="button" class="btn btn-outline-secondary btn-sm mr-2"
(click)="expand = !expand">
<svg class="buttonicon" fill="currentColor" *ngIf="!expand">
<use xlink:href="assets/bootstrap-icons.svg#caret-down" />
</svg>
<svg class="buttonicon" fill="currentColor" *ngIf="expand">
<use xlink:href="assets/bootstrap-icons.svg#caret-up" />
</svg>
</button>
{{title}}
</h6>
<div #collapse="ngbCollapse" [(ngbCollapse)]="!expand">
<table class="table table-borderless">
<tbody>
<tr *ngFor="let m of metadata">
<td>{{m.prefix}}:{{m.key}}</td>
<td class="metadata-column">{{m.value}}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -0,0 +1,3 @@
.metadata-column {
overflow-wrap: anywhere;
}

View File

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

View File

@@ -0,0 +1,23 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-metadata-collapse',
templateUrl: './metadata-collapse.component.html',
styleUrls: ['./metadata-collapse.component.scss']
})
export class MetadataCollapseComponent implements OnInit {
constructor() { }
expand = false
@Input()
metadata
@Input()
title = $localize`Metadata`
ngOnInit(): void {
}
}