mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
added paperless ui
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
.result-content {
|
||||
color: darkgray;
|
||||
}
|
||||
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
}
|
@@ -0,0 +1,38 @@
|
||||
<div class="card mb-3 bg-light">
|
||||
<div class="row no-gutters">
|
||||
<div class="col-md-2 d-none d-lg-block">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img">
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="card-body">
|
||||
|
||||
<h5 class="card-title">{{document.title}}<app-tag [tag]="t" *ngFor="let t of document.tags" class="ml-1"></app-tag></h5>
|
||||
<p class="card-text">
|
||||
<app-result-hightlight *ngIf="getDetailsAsHighlight()" class="result-content" [highlights]="getDetailHighlight()"></app-result-hightlight>
|
||||
<span *ngIf="getDetailsAsString()" class="result-content">{{getDetailsString()}}</span>
|
||||
</p>
|
||||
|
||||
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||
<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>
|
||||
Edit
|
||||
</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"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">{{document.created | date}}</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentCardLargeComponent } from './document-card-large.component';
|
||||
|
||||
describe('DocumentCardLargeComponent', () => {
|
||||
let component: DocumentCardLargeComponent;
|
||||
let fixture: ComponentFixture<DocumentCardLargeComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DocumentCardLargeComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentCardLargeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { Component, Input, OnInit } 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';
|
||||
import { SearchResultHighlightedText } from 'src/app/services/rest/search.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-large',
|
||||
templateUrl: './document-card-large.component.html',
|
||||
styleUrls: ['./document-card-large.component.css']
|
||||
})
|
||||
export class DocumentCardLargeComponent implements OnInit {
|
||||
|
||||
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
@Input()
|
||||
details: any
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
getDetailsAsString() {
|
||||
if (typeof this.details === 'string') {
|
||||
return this.details.substring(0, 500)
|
||||
}
|
||||
}
|
||||
|
||||
getDetailsAsHighlight() {
|
||||
//TODO: this is not an exact typecheck, can we do better
|
||||
if (this.details instanceof Array) {
|
||||
return this.details
|
||||
}
|
||||
}
|
||||
|
||||
getThumbUrl() {
|
||||
return this.documentService.getThumbUrl(this.document.id)
|
||||
}
|
||||
|
||||
getDownloadUrl() {
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
.doc-img {
|
||||
object-fit: cover;
|
||||
object-position: top;
|
||||
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
<div class="col-auto mb-3">
|
||||
<div class="card h-100 bg-light" style="width: 14rem">
|
||||
<div style="height: 10rem; overflow: hidden;">
|
||||
<img [src]="getThumbUrl()" class="card-img doc-img"/>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p 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></p>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="btn-group">
|
||||
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
|
||||
<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">
|
||||
<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"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<small class="text-muted">{{document.created | date}}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentCardSmallComponent } from './document-card-small.component';
|
||||
|
||||
describe('DocumentCardSmallComponent', () => {
|
||||
let component: DocumentCardSmallComponent;
|
||||
let fixture: ComponentFixture<DocumentCardSmallComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DocumentCardSmallComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentCardSmallComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,27 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { DocumentService } from 'src/app/services/rest/document.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-card-small',
|
||||
templateUrl: './document-card-small.component.html',
|
||||
styleUrls: ['./document-card-small.component.css']
|
||||
})
|
||||
export class DocumentCardSmallComponent implements OnInit {
|
||||
|
||||
constructor(private documentService: DocumentService) { }
|
||||
|
||||
@Input()
|
||||
document: PaperlessDocument
|
||||
|
||||
ngOnInit(): void {
|
||||
}
|
||||
|
||||
getThumbUrl() {
|
||||
return this.documentService.getThumbUrl(this.document.id)
|
||||
}
|
||||
|
||||
getDownloadUrl() {
|
||||
return this.documentService.getDownloadUrl(this.document.id)
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<app-page-header title="Documents">
|
||||
|
||||
<div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="displayMode" (ngModelChange)="saveDisplayMode()">
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="details">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#list-ul"/>
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="smallCards">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#grid"/>
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="largeCards">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#hdd-stack"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="docs.currentSortDirection" (ngModelChange)="reload()">
|
||||
<div ngbDropdown class="btn-group">
|
||||
<button class="btn btn-outline-secondary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
|
||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
||||
<button *ngFor="let f of getSortFields()" ngbDropdownItem (click)="setSort(f.field)">{{f.name}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="asc">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down"/>
|
||||
</svg>
|
||||
</label>
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
<input ngbButton type="radio" class="btn btn-sm" value="des">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt"/>
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" (click)="showFilter=!showFilter">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
|
||||
</svg>
|
||||
Filter
|
||||
</button>
|
||||
</app-page-header>
|
||||
|
||||
<div class="card w-100 mb-3" [hidden]="!showFilter">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Filter</h5>
|
||||
<app-filter-editor [(ruleSet)]="filter" (apply)="applyFilter()"></app-filter-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ngb-pagination [pageSize]="25" [collectionSize]="docs.collectionSize" [(page)]="docs.currentPage" (pageChange)="reload()"
|
||||
aria-label="Default pagination"></ngb-pagination>
|
||||
|
||||
<div *ngIf="displayMode == 'largeCards'">
|
||||
<app-document-card-large *ngFor="let d of docs.documents"
|
||||
[document]="d"
|
||||
[details]="d.content">
|
||||
</app-document-card-large>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-sm" *ngIf="displayMode == 'details'">
|
||||
<thead>
|
||||
<th>ASN</th>
|
||||
<th>Correspondent</th>
|
||||
<th>Title</th>
|
||||
<th>Document type</th>
|
||||
<th>Date created</th>
|
||||
<th>Date added</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let d of docs.documents" routerLink="/documents/{{d.id}}">
|
||||
<td>{{d.archive_serial_number}}</td>
|
||||
<td>{{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>{{d.document_type ? d.document_type.name : ''}}</td>
|
||||
<td>{{d.created | date}}</td>
|
||||
<td>{{d.added | date}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="row justify-content-left" *ngIf="displayMode == 'smallCards'">
|
||||
<app-document-card-small [document]="d" *ngFor="let d of docs.documents"></app-document-card-small>
|
||||
</div>
|
||||
|
||||
<p *ngIf="docs.documents.length == 0" class="mx-auto">No results</p>
|
@@ -0,0 +1,25 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DocumentListComponent } from './document-list.component';
|
||||
|
||||
describe('DocumentListComponent', () => {
|
||||
let component: DocumentListComponent;
|
||||
let fixture: ComponentFixture<DocumentListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ DocumentListComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(DocumentListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -0,0 +1,51 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
|
||||
import { FilterRuleSet } from '../filter-editor/filter-editor.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-document-list',
|
||||
templateUrl: './document-list.component.html',
|
||||
styleUrls: ['./document-list.component.css']
|
||||
})
|
||||
export class DocumentListComponent implements OnInit {
|
||||
|
||||
constructor(
|
||||
public docs: DocumentListViewService) { }
|
||||
|
||||
displayMode = 'smallCards' // largeCards, smallCards, details
|
||||
|
||||
filter = new FilterRuleSet()
|
||||
showFilter = false
|
||||
|
||||
getSortFields() {
|
||||
return DocumentListViewService.SORT_FIELDS
|
||||
}
|
||||
|
||||
setSort(field: string) {
|
||||
this.docs.currentSortField = field
|
||||
this.reload()
|
||||
}
|
||||
|
||||
saveDisplayMode() {
|
||||
localStorage.setItem('document-list:displayMode', this.displayMode)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (localStorage.getItem('document-list:displayMode') != null) {
|
||||
this.displayMode = localStorage.getItem('document-list:displayMode')
|
||||
}
|
||||
this.filter = this.docs.currentFilter.clone()
|
||||
this.showFilter = this.filter.rules.length > 0
|
||||
this.reload()
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.docs.reload()
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.docs.setFilter(this.filter.clone())
|
||||
this.reload()
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user