added paperless ui

This commit is contained in:
Jonas Winkler
2020-10-27 01:10:18 +01:00
parent e24baf5811
commit 8693bee4ac
173 changed files with 18693 additions and 0 deletions

View File

@@ -0,0 +1,42 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { AppFrameComponent } from './components/app-frame/app-frame.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { DocumentDetailComponent } from './components/document-detail/document-detail.component';
import { DocumentListComponent } from './components/document-list/document-list.component';
import { LoginComponent } from './components/login/login.component';
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component';
import { LogsComponent } from './components/manage/logs/logs.component';
import { SettingsComponent } from './components/manage/settings/settings.component';
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { SearchComponent } from './components/search/search.component';
import { AuthGuardService } from './services/auth-guard.service';
const routes: Routes = [
{path: '', redirectTo: 'dashboard', pathMatch: 'full'},
{path: '', component: AppFrameComponent, children: [
{path: 'dashboard', component: DashboardComponent, canActivate: [AuthGuardService] },
{path: 'documents', component: DocumentListComponent, canActivate: [AuthGuardService] },
{path: 'view/:name', component: DocumentListComponent, canActivate: [AuthGuardService] },
{path: 'search', component: SearchComponent, canActivate: [AuthGuardService] },
{path: 'documents/:id', component: DocumentDetailComponent, canActivate: [AuthGuardService] },
{path: 'tags', component: TagListComponent, canActivate: [AuthGuardService] },
{path: 'documenttypes', component: DocumentTypeListComponent, canActivate: [AuthGuardService] },
{path: 'correspondents', component: CorrespondentListComponent, canActivate: [AuthGuardService] },
{path: 'logs', component: LogsComponent, canActivate: [AuthGuardService] },
{path: 'settings', component: SettingsComponent, canActivate: [AuthGuardService] },
]},
{path: 'login', component: LoginComponent },
{path: '404', component: NotFoundComponent},
{path: '**', redirectTo: '/404', pathMatch: 'full'}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

View File

View File

@@ -0,0 +1,3 @@
<app-toasts></app-toasts>
<router-outlet></router-outlet>

View File

@@ -0,0 +1,35 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterTestingModule
],
declarations: [
AppComponent
],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'paperless-ui'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('paperless-ui');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('.content span').textContent).toContain('paperless-ui app is running!');
});
});

View File

@@ -0,0 +1,14 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor () {
}
}

View File

@@ -0,0 +1,82 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { DocumentListComponent } from './components/document-list/document-list.component';
import { DocumentDetailComponent } from './components/document-detail/document-detail.component';
import { DashboardComponent } from './components/dashboard/dashboard.component';
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component';
import { LogsComponent } from './components/manage/logs/logs.component';
import { SettingsComponent } from './components/manage/settings/settings.component';
import { LoginComponent } from './components/login/login.component';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { SafePipe } from './pipes/safe.pipe';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component';
import { DeleteDialogComponent } from './components/common/delete-dialog/delete-dialog.component';
import { CorrespondentEditDialogComponent } from './components/manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { TagEditDialogComponent } from './components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
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 { 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';
import { FilterEditorComponent } from './components/filter-editor/filter-editor.component';
import { AuthInterceptor } from './services/auth.interceptor';
import { DocumentCardLargeComponent } from './components/document-list/document-card-large/document-card-large.component';
import { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component';
@NgModule({
declarations: [
AppComponent,
DocumentListComponent,
DocumentDetailComponent,
DashboardComponent,
TagListComponent,
CorrespondentListComponent,
DocumentTypeListComponent,
LogsComponent,
SettingsComponent,
LoginComponent,
SafePipe,
NotFoundComponent,
CorrespondentEditDialogComponent,
DeleteDialogComponent,
TagEditDialogComponent,
DocumentTypeEditDialogComponent,
TagComponent,
SearchComponent,
ResultHightlightComponent,
PageHeaderComponent,
AppFrameComponent,
ToastsComponent,
FilterEditorComponent,
DocumentCardLargeComponent,
DocumentCardSmallComponent
],
imports: [
BrowserModule,
AppRoutingModule,
NgbModule,
HttpClientModule,
FormsModule,
ReactiveFormsModule
],
providers: [
DatePipe,
{
provide: HTTP_INTERCEPTORS,
useClass: AuthInterceptor,
multi: true
}
],
bootstrap: [AppComponent]
})
export class AppModule { }

View File

@@ -0,0 +1,84 @@
/*
* Sidebar
*/
.sidebar {
position: fixed;
top: 0;
bottom: 0;
left: 0;
z-index: 100; /* Behind the navbar */
padding: 48px 0 0; /* Height of navbar */
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1);
}
@media (max-width: 767.98px) {
.sidebar {
top: 5rem;
}
}
.sidebar-sticky {
position: relative;
top: 0;
height: calc(100vh - 48px);
padding-top: .5rem;
overflow-x: hidden;
overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */
}
@supports ((position: -webkit-sticky) or (position: sticky)) {
.sidebar-sticky {
position: -webkit-sticky;
position: sticky;
}
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
}
.sidebar .nav-link .sidebaricon {
margin-right: 4px;
color: #999;
}
.sidebar .nav-link.active {
color: #007bff;
}
.sidebar .nav-link:hover .sidebaricon,
.sidebar .nav-link.active .sidebaricon {
color: inherit;
}
.sidebar-heading {
font-size: .75rem;
text-transform: uppercase;
}
/*
* Navbar
*/
.navbar-brand {
padding-top: .75rem;
padding-bottom: .75rem;
font-size: 1rem;
background-color: rgba(0, 0, 0, .25);
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .25);
}
.navbar .navbar-toggler {
top: .25rem;
right: 1rem;
}
.navbar .form-control {
padding: .75rem 1rem;
border-width: 0;
border-radius: 0;
}

View File

@@ -0,0 +1,112 @@
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<span class="navbar-brand col-md-3 col-lg-2 mr-0 px-3" href="#">Paperless</span>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-toggle="collapse"
data-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<form (ngSubmit)="search()" class="w-100">
<input class="form-control form-control-dark" type="text" placeholder="Search" aria-label="Search"
[formControl]="searchField">
</form>
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" (click)="logout()" style="cursor: pointer;">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#door-closed"/>
</svg>
Logout
</a>
</li>
</ul>
</nav>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" routerLink="dashboard" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#house"/>
</svg>
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="documents" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#files"/>
</svg>
Documents
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='openDocuments.length > 0'>
<span>Open documents</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item" *ngFor='let d of openDocuments'>
<a class="nav-link" routerLink="documents/{{d.id}}" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#file-text"/>
</svg>
{{d.title}}
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Manage</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" routerLink="correspondents" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#person"/>
</svg>
Correspondents
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="tags" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#tags"/>
</svg>
Tags
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="documenttypes" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#hash"/>
</svg>
Document types
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="logs" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#text-left"/>
</svg>
Logs
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg>
Settings
</a>
</li>
</ul>
</div>
</nav>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<router-outlet></router-outlet>
</main>
</div>
</div>

View File

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

View File

@@ -0,0 +1,41 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Router } from '@angular/router';
import { Subscription } from 'rxjs';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { AuthService } from 'src/app/services/auth.service';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
@Component({
selector: 'app-app-frame',
templateUrl: './app-frame.component.html',
styleUrls: ['./app-frame.component.css']
})
export class AppFrameComponent implements OnInit, OnDestroy {
constructor (public router: Router, private openDocumentsService: OpenDocumentsService, private authService: AuthService) {
}
searchField = new FormControl('')
openDocuments: PaperlessDocument[] = []
openDocumentsSubscription: Subscription
search() {
this.router.navigate(['search'], {queryParams: {query: this.searchField.value}})
}
logout() {
this.authService.logout()
}
ngOnInit() {
this.openDocumentsSubscription = this.openDocumentsService.getOpenDocuments().subscribe(docs => this.openDocuments = docs)
}
ngOnDestroy() {
this.openDocumentsSubscription.unsubscribe()
}
}

View File

@@ -0,0 +1,14 @@
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{title}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancelClicked()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p><b>{{message}}</b></p>
<p *ngIf="message2">{{message2}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()">Cancel</button>
<button type="button" class="btn btn-danger" (click)="deleteClicked.emit()">Delete</button>
</div>

View File

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

View File

@@ -0,0 +1,31 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@Component({
selector: 'app-delete-dialog',
templateUrl: './delete-dialog.component.html',
styleUrls: ['./delete-dialog.component.css']
})
export class DeleteDialogComponent implements OnInit {
constructor(public activeModal: NgbActiveModal) { }
@Output()
public deleteClicked = new EventEmitter()
@Input()
title = "Delete confirmation"
@Input()
message = "Do you really want to delete this?"
@Input()
message2
ngOnInit(): void {
}
cancelClicked() {
this.activeModal.close()
}
}

View File

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

View File

@@ -0,0 +1,71 @@
import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Form, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
import { Toast, ToastService } from 'src/app/services/toast.service';
@Directive()
export abstract class EditDialogComponent<T extends ObjectWithId> implements OnInit {
constructor(
private service: AbstractPaperlessService<T>,
private activeModal: NgbActiveModal,
private toastService: ToastService,
private entityName: string) { }
@Input()
dialogMode: string = 'create'
@Input()
object: T
@Output()
success = new EventEmitter()
abstract getForm(): FormGroup
objectForm: FormGroup = this.getForm()
ngOnInit(): void {
if (this.object != null) {
this.objectForm.patchValue(this.object)
}
}
getTitle() {
switch (this.dialogMode) {
case 'create':
return "Create new " + this.entityName
case 'edit':
return "Edit " + this.entityName
default:
break;
}
}
save() {
var newObject = Object.assign(Object.assign({}, this.object), this.objectForm.value)
var serverResponse: Observable<T>
switch (this.dialogMode) {
case 'create':
serverResponse = this.service.create(newObject)
break;
case 'edit':
serverResponse = this.service.update(newObject)
default:
break;
}
serverResponse.subscribe(result => {
this.activeModal.close()
this.success.emit(result)
}, error => {
this.toastService.showToast(Toast.make("Error", `Could not save ${this.entityName}: ${error.error.name}`))
})
}
cancel() {
this.activeModal.close()
}
}

View File

@@ -0,0 +1,6 @@
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">{{title}}</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<ng-content></ng-content>
</div>
</div>

View File

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

View File

@@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'app-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.css']
})
export class PageHeaderComponent implements OnInit {
constructor() { }
@Input()
title: string = ""
ngOnInit(): void {
}
}

View File

@@ -0,0 +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>

View File

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

View File

@@ -0,0 +1,29 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
@Component({
selector: 'app-tag',
templateUrl: './tag.component.html',
styleUrls: ['./tag.component.css']
})
export class TagComponent implements OnInit {
constructor() { }
@Input()
tag: PaperlessTag
@Input()
clickable: boolean = false
@Output()
click = new EventEmitter()
ngOnInit(): void {
}
getColour() {
return PaperlessTag.COLOURS.find(c => c.id == this.tag.colour)
}
}

View File

@@ -0,0 +1,7 @@
:host {
position: fixed;
top: 0;
right: 0;
margin: 0.5em;
z-index: 1200;
}

View File

@@ -0,0 +1,6 @@
<ngb-toast
*ngFor="let toast of toasts"
[header]="toast.title" [autohide]="true" [delay]="toast.delay"
(hide)="toastService.closeToast(toast)">
{{toast.content}}
</ngb-toast>

View File

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

View File

@@ -0,0 +1,26 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { Toast, ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-toasts',
templateUrl: './toasts.component.html',
styleUrls: ['./toasts.component.css']
})
export class ToastsComponent implements OnInit, OnDestroy {
constructor(private toastService: ToastService) { }
subscription: Subscription
toasts: Toast[] = []
ngOnDestroy(): void {
this.subscription.unsubscribe()
}
ngOnInit(): void {
this.subscription = this.toastService.getToasts().subscribe(toasts => this.toasts = toasts)
}
}

View File

@@ -0,0 +1,29 @@
<app-page-header title="Dashboard">
<button type="button" class="btn btn-sm btn-outline-secondary">
Show Documents with Tag
</button>
</app-page-header>
<p>This space for rent...</p>
<div class='row'>
<div class="col">
<h4>Recent Documents</h4>
<p>Recent docs</p>
</div>
</div>
<div class='row'>
<div class="col">
<h4>Documents with tag <span class="badge badge-danger">TODO</span></h4>
<p>...</p>
</div>
</div>
<div class='row'>
<div class="col-4">
<h4>Statistics</h4>
</div>
<div class="col-8">
<h4>Upload new Document</h4>
</div>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
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-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,124 @@
<app-page-header [(title)]="title">
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>
Delete
</button>
<a [href]="downloadUrl" class="btn btn-sm btn-outline-secondary mr-2">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#download" />
</svg>
Download
</a>
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="close()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x" />
</svg>
Close
</button>
</app-page-header>
<div class="row">
<div class="col-xl">
<form [formGroup]='documentForm' (ngSubmit)="save()">
<div class="form-group">
<label for="title">Title</label>
<input type="text" class="form-control" id="title" formControlName='title'>
</div>
<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>
<div class="form-row">
<div class="form-group col">
<label for="created_date">Date created</label>
<input type="date" class="form-control" id="created_date" formControlName='created_date'>
</div>
<div class="form-group col">
<label for="created_time">Time created</label>
<input type="time" class="form-control" id="created_time" formControlName='created_time'>
</div>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea class="form-control" id="content" rows="5" formControlName='content'></textarea>
</div>
<div class="form-group">
<label for="correspondent">Correspondent</label>
<div class="input-group">
<select class="form-control" id="correspondent" formControlName="correspondent_id">
<option [ngValue]="null">---</option>
<option *ngFor='let c of correspondents' [ngValue]="c.id">{{c.name}}</option>
</select>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createCorrespondent()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="document_type">Document Type</label>
<div class="input-group">
<select class="form-control" id="document_type" formControlName="document_type_id">
<option [ngValue]="null">---</option>
<option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option>
</select>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createDocumentType()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="exampleFormControlTextarea1">Tags</label>
<div class="input-group">
<div class="form-control">
<span *ngFor="let tag of documentForm.value.tags">
<app-tag [tag]="tag" [clickable]="true" (click)="removeTag(tag)"></app-tag>
&nbsp;
</span>
</div>
<div class="input-group-append" ngbDropdown>
<button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag)">
<app-tag [tag]="tag"></app-tag>
</button>
</div>
</div>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
</div>
</div>
</div>
<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>
<div class="col-xl">
<object [data]="previewUrl | safe" type="application/pdf" width="100%" height="100%">
<p>Your browser does not support PDFs.
<a href="previewUrl">Download the PDF</a>.</p>
</object>
</div>
</div>

View File

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

View File

@@ -0,0 +1,177 @@
import { DatePipe } 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 { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { 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',
templateUrl: './document-detail.component.html',
styleUrls: ['./document-detail.component.css']
})
export class DocumentDetailComponent implements OnInit {
documentId: number
document: PaperlessDocument
title: string
previewUrl: string
downloadUrl: string
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
tags: PaperlessTag[]
documentForm: FormGroup = new FormGroup({
title: new FormControl(''),
content: new FormControl(''),
created_date: new FormControl(),
created_time: new FormControl(),
correspondent_id: new FormControl(),
document_type_id: new FormControl(),
archive_serial_number: new FormControl(),
tags: new FormControl([])
})
constructor(
private documentsService: DocumentService,
private route: ActivatedRoute,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private datePipe: DatePipe,
private router: Router,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService) { }
ngOnInit(): void {
this.correspondentService.list().subscribe(result => this.correspondents = result.results)
this.documentTypeService.list().subscribe(result => this.documentTypes = result.results)
this.tagService.list().subscribe(result => this.tags = result.results)
this.route.paramMap.subscribe(paramMap => {
this.documentId = +paramMap.get('id')
this.previewUrl = this.documentsService.getPreviewUrl(this.documentId)
this.downloadUrl = this.documentsService.getDownloadUrl(this.documentId)
this.documentsService.get(this.documentId).subscribe(doc => {
this.openDocumentService.openDocument(doc)
this.document = doc
this.title = doc.title
this.documentForm.patchValue(doc)
this.documentForm.get('created_date').patchValue(this.datePipe.transform(doc.created, 'yyyy-MM-dd'))
this.documentForm.get('created_time').patchValue(this.datePipe.transform(doc.created, 'HH:mm:ss'))
}, error => {this.router.navigate(['404'])})
})
}
createTag() {
var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.success.subscribe(newTag => {
this.tagService.list().subscribe(tags => {
this.tags = tags.results
this.documentForm.get('tags_id').value.push(newTag.id)
})
})
}
createDocumentType() {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.success.subscribe(newDocumentType => {
this.documentTypeService.list().subscribe(documentTypes => {
this.documentTypes = documentTypes.results
this.documentForm.get('document_type_id').setValue(newDocumentType.id)
})
})
}
createCorrespondent() {
var modal = this.modalService.open(CorrespondentEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.success.subscribe(newCorrespondent => {
this.correspondentService.list().subscribe(correspondents => {
this.correspondents = correspondents.results
this.documentForm.get('correspondent_id').setValue(newCorrespondent.id)
})
})
}
getTag(id: number): PaperlessTag {
return this.tags.find(tag => tag.id == id)
}
getColour(id: number) {
return PaperlessTag.COLOURS.find(c => c.id == this.getTag(id).colour)
}
addTag(id: number) {
if (this.documentForm.value.tags.indexOf(id) == -1) {
this.documentForm.value.tags.push(id)
}
}
removeTag(id: number) {
var index = this.documentForm.value.tags.indexOf(id)
if (index > -1) {
this.documentForm.value.tags.splice(index, 1)
}
}
save() {
var newDocument = Object.assign(Object.assign({}, this.document), this.documentForm.value)
console.log(this.document)
console.log(newDocument)
this.documentsService.update(newDocument).subscribe(result => {
this.close()
})
}
saveEditNext() {
var newDocument = Object.assign(Object.assign({}, this.document), this.documentForm.value)
this.documentsService.update(newDocument).subscribe(result => {
this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
if (nextDocId) {
this.openDocumentService.closeDocument(this.document)
this.router.navigate(['documents', nextDocId])
}
})
})
}
close() {
this.openDocumentService.closeDocument(this.document)
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(() => {
this.documentsService.delete(this.document).subscribe(() => {
modal.close()
this.close()
})
})
}
hasNext() {
return this.documentListViewService.hasNext(this.documentId)
}
}

View File

@@ -0,0 +1,11 @@
.result-content {
color: darkgray;
}
.doc-img {
object-fit: cover;
object-position: top;
height: 100%;
position: absolute;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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)
}
}

View File

@@ -0,0 +1,5 @@
.doc-img {
object-fit: cover;
object-position: top;
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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)
}
}

View File

@@ -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>

View File

@@ -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();
});
});

View File

@@ -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()
}
}

View File

@@ -0,0 +1,51 @@
<div *ngFor="let rule of ruleSet.rules" class="form-row form-group">
<div class="col">
<select class="form-control form-control-sm" [(ngModel)]="rule.type" (change)="rule.value = null">
<option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option>
</select>
</div>
<div class="col">
<input *ngIf="rule.type.datatype == 'string'" type="text" class="form-control form-control-sm" [(ngModel)]="rule.value">
<input *ngIf="rule.type.datatype == 'number'" type="number" class="form-control form-control-sm" [(ngModel)]="rule.value">
<input *ngIf="rule.type.datatype == 'date'" type="date" class="form-control form-control-sm" [(ngModel)]="rule.value">
<select *ngIf="rule.type.datatype == 'tag'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let t of tags" [ngValue]="t.id">{{t.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'documentType'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let dt of documentTypes" [ngValue]="dt.id">{{dt.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'correspondent'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option *ngFor="let c of correspondents" [ngValue]="c.id">{{c.name}}</option>
</select>
<select *ngIf="rule.type.datatype == 'boolean'" class="form-control form-control-sm" [(ngModel)]="rule.value">
<option [ngValue]="true">Yes</option>
<option [ngValue]="false">No</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-outline-secondary" (click)="removeRuleClicked(rule)">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
</button>
</div>
</div>
<div class="form-row form-group">
<div class="col">
<select [(ngModel)]="selectedRuleType" class="form-control form-control-sm">
<option *ngFor="let ruleType of getRuleTypes()" [ngValue]="ruleType">{{ruleType.name}}</option>
</select>
</div>
<div class="col-auto">
<button (click)="newRuleClicked()" class="btn btn-sm btn-outline-secondary">Add</button>
</div>
<div class="col-auto">
<button (click)="applyClicked()" class="btn btn-sm btn-outline-secondary">Apply</button>
</div>
</div>

View File

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

View File

@@ -0,0 +1,118 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
export interface FilterRuleType {
name: string
filtervar: string
datatype: string //number, string, boolean, date
}
export interface FilterRule {
type: FilterRuleType
value: any
}
export class FilterRuleSet {
static RULE_TYPES: FilterRuleType[] = [
{name: "Title contains", filtervar: "title__icontains", datatype: "string"},
{name: "Content contains", filtervar: "content__icontains", datatype: "string"},
{name: "ASN is", filtervar: "archive_serial_number", datatype: "number"},
{name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent"},
{name: "Document type is", filtervar: "document_type__id", datatype: "document_type"},
{name: "Has tag", filtervar: "tags__id", datatype: "tag"},
{name: "Has any tag", filtervar: "is_tagged", datatype: "boolean"},
{name: "Date created before", filtervar: "created__date__lt", datatype: "date"},
{name: "Date created after", filtervar: "created__date__gt", datatype: "date"},
{name: "Year created is", filtervar: "created__year", datatype: "number"},
{name: "Month created is", filtervar: "created__month", datatype: "number"},
{name: "Day created is", filtervar: "created__day", datatype: "number"},
{name: "Date added before", filtervar: "added__date__lt", datatype: "date"},
{name: "Date added after", filtervar: "added__date__gt", datatype: "date"},
{name: "Date modified before", filtervar: "modified__date__lt", datatype: "date"},
{name: "Date modified after", filtervar: "modified__date__gt", datatype: "date"},
]
rules: FilterRule[] = []
toQueryParams() {
let params = {}
for (let rule of this.rules) {
params[rule.type.filtervar] = rule.value
}
return params
}
clone(): FilterRuleSet {
let newRuleSet = new FilterRuleSet()
for (let rule of this.rules) {
newRuleSet.rules.push({type: rule.type, value: rule.value})
}
return newRuleSet
}
constructor() { }
}
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.css']
})
export class FilterEditorComponent implements OnInit {
constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { }
@Input()
ruleSet = new FilterRuleSet()
@Output()
ruleSetChange = new EventEmitter<FilterRuleSet>()
@Output()
apply = new EventEmitter()
selectedRuleType: FilterRuleType = FilterRuleSet.RULE_TYPES[0]
correspondents: PaperlessCorrespondent[] = []
tags: PaperlessTag[] = []
documentTypes: PaperlessDocumentType[] = []
newRuleClicked() {
this.ruleSet.rules.push({type: this.selectedRuleType, value: null})
}
removeRuleClicked(rule) {
let index = this.ruleSet.rules.findIndex(r => r == rule)
if (index > -1) {
this.ruleSet.rules.splice(index, 1)
}
}
applyClicked() {
this.apply.next()
}
ngOnInit(): void {
this.correspondentService.list().subscribe(result => {this.correspondents = result.results})
this.tagService.list().subscribe(result => this.tags = result.results)
this.documentTypeService.list().subscribe(result => this.documentTypes = result.results)
}
getRuleTypes() {
return FilterRuleSet.RULE_TYPES
}
}

View File

@@ -0,0 +1,44 @@
.form-signin-container {
top: 0;
bottom: 0;
left: 0;
right: 0;
position: fixed;
background-color: #f5f5f5;
}
.form-signin {
max-width: 330px;
height: auto;
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
text-align: center;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="text"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}

View File

@@ -0,0 +1,17 @@
<div class="form-signin-container">
<form class="form-signin mt-5" [formGroup]="loginForm" (ngSubmit)="loginClicked()">
<img class="mb-4" src="assets/logo.svg" alt="" width="100%">
<h1 class="h3 mb-3 font-weight-normal">Login</h1>
<label for="inputUsername" class="sr-only">Username</label>
<input type="text" id="inputUsername" class="form-control" placeholder="Username" required autofocus formControlName="username">
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="inputPassword" class="form-control" placeholder="Password" required formControlName="password">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" formControlName="rememberMe"> Remember me
</label>
</div>
<button class="btn btn-lg btn-primary btn-block mb-4" type="submit">Login</button>
<p><a href="/admin/">Go to admin interface</a></p>
</form>
</div>

View File

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

View File

@@ -0,0 +1,34 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Router } from '@angular/router';
import { AuthService } from 'src/app/services/auth.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
constructor(private auth: AuthService, private router: Router, private toastService: ToastService) { }
loginForm = new FormGroup({
username: new FormControl(''),
password: new FormControl(''),
rememberMe: new FormControl(false)
})
ngOnInit(): void {
}
loginClicked() {
this.auth.login(this.loginForm.value.username, this.loginForm.value.password, this.loginForm.value.rememberMe).subscribe(result => {
this.router.navigate([''])
}, (error) => {
this.toastService.showToast(Toast.make("Error", "result: " + JSON.stringify(error.error)))
}
)
}
}

View File

@@ -0,0 +1,23 @@
<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="name">Name</label>
<input id="name" class="form-control" formControlName="name" required ngbAutofocus>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="automatic_classification"
formControlName="automatic_classification">
<label class="form-check-label" for="automatic_classification">Automatic Classification</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>

View File

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

View File

@@ -0,0 +1,27 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-correspondent-edit-dialog',
templateUrl: './correspondent-edit-dialog.component.html',
styleUrls: ['./correspondent-edit-dialog.component.css']
})
export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> {
constructor(service: CorrespondentService, activeModal: NgbActiveModal, toastService: ToastService) {
super(service, activeModal, toastService, 'correspondent')
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(''),
automatic_classification: new FormControl(true)
})
}
}

View File

@@ -0,0 +1,33 @@
<app-page-header title="Correspondents">
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()">
Create
</button>
</app-page-header>
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Automatic Classification</th>
<th scope="col">Document count</th>
<th scope="col">Last correspondence</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let correspondent of data">
<td scope="row">{{ correspondent.name }}</td>
<td scope="row">{{ correspondent.automatic_classification }}</td>
<td scope="row">{{ correspondent.document_count }}</td>
<td scope="row">{{ correspondent.last_correspondence | date }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

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

View File

@@ -0,0 +1,20 @@
import { Component } from '@angular/core';
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 { GenericListComponent } from '../generic-list/generic-list.component';
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
@Component({
selector: 'app-correspondent-list',
templateUrl: './correspondent-list.component.html',
styleUrls: ['./correspondent-list.component.css']
})
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
constructor(correspondentsService: CorrespondentService,
modalService: NgbModal) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
}

View File

@@ -0,0 +1,23 @@
<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="name">Name</label>
<input id="name" class="form-control" formControlName="name" required ngbAutofocus>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="automatic_classification"
formControlName="automatic_classification">
<label class="form-check-label" for="automatic_classification">Automatic Classification</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>

View File

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

View File

@@ -0,0 +1,27 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-document-type-edit-dialog',
templateUrl: './document-type-edit-dialog.component.html',
styleUrls: ['./document-type-edit-dialog.component.css']
})
export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, activeModal: NgbActiveModal, toastService: ToastService) {
super(service, activeModal, toastService, 'document type')
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(''),
automatic_classification: new FormControl(true)
})
}
}

View File

@@ -0,0 +1,32 @@
<app-page-header title="Document types">
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()">
Create
</button>
</app-page-header>
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()"
aria-label="Default pagination"></ngb-pagination>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Automatic Classification</th>
<th scope="col">Document count</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let correspondent of data">
<td scope="row">{{ correspondent.name }}</td>
<td scope="row">{{ correspondent.automatic_classification }}</td>
<td scope="row">{{ correspondent.document_count }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(correspondent)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(correspondent)">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

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

View File

@@ -0,0 +1,19 @@
import { Component, OnInit } from '@angular/core';
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 { GenericListComponent } from '../generic-list/generic-list.component';
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
@Component({
selector: 'app-document-type-list',
templateUrl: './document-type-list.component.html',
styleUrls: ['./document-type-list.component.css']
})
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, modalService: NgbModal) {
super(service, modalService, DocumentTypeEditDialogComponent)
}
}

View File

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

View File

@@ -0,0 +1,62 @@
import { Directive, OnInit } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
@Directive()
export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit {
constructor(
private service: AbstractPaperlessService<T>,
private modalService: NgbModal,
private editDialogComponent: any) {
}
public data: T[] = []
public page = 1
public collectionSize = 0
ngOnInit(): void {
this.reloadData()
}
reloadData() {
this.service.list(this.page).subscribe(c => {
this.data = c.results
this.collectionSize = c.count
});
}
openCreateDialog() {
var activeModal = this.modalService.open(this.editDialogComponent, {backdrop: 'static'})
activeModal.componentInstance.dialogMode = 'create'
activeModal.componentInstance.success.subscribe(o => {
this.reloadData()
})
}
openEditDialog(object: T) {
var activeModal = this.modalService.open(this.editDialogComponent, {backdrop: 'static'})
activeModal.componentInstance.object = object
activeModal.componentInstance.dialogMode = 'edit'
activeModal.componentInstance.success.subscribe(o => {
this.reloadData()
})
}
openDeleteDialog(object: T) {
var activeModal = this.modalService.open(DeleteDialogComponent, {backdrop: 'static'})
activeModal.componentInstance.message = `Do you really want to delete ${object}?`
activeModal.componentInstance.message2 = "Associated documents will not be deleted."
activeModal.componentInstance.deleteClicked.subscribe(() => {
this.service.delete(object).subscribe(_ => {
activeModal.close()
this.reloadData()
})
}
)
}
}

View File

@@ -0,0 +1,2 @@
<app-page-header title="Logs">
</app-page-header>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LogsComponent } from './logs.component';
describe('LogsComponent', () => {
let component: LogsComponent;
let fixture: ComponentFixture<LogsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LogsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LogsComponent);
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-logs',
templateUrl: './logs.component.html',
styleUrls: ['./logs.component.css']
})
export class LogsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,6 @@
<app-page-header title="Settings">
</app-page-header>
<p>items per page, documents per view type</p>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsComponent } from './settings.component';
describe('SettingsComponent', () => {
let component: SettingsComponent;
let fixture: ComponentFixture<SettingsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SettingsComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SettingsComponent);
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-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.css']
})
export class SettingsComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,34 @@
<form [formGroup]="objectForm" class="needs-validation" novalidate (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" class="close" aria-label="Close" (click)="cancel()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label for="name">Name</label>
<input id="name" class="form-control" formControlName="name" required ngbAutofocus>
</div>
<div class="form-group">
<label for="colour">Colour</label>
<select id="colour" class="form-control" required formControlName="colour" [style.color]="getColor(objectForm.value.colour).textColor" [style.background]="getColor(objectForm.value.colour).value">
<option *ngFor="let colour of getColours()" [ngValue]="colour.id" class="form-control">{{colour.name}}</option>
</select>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="is_inbox_tag"
formControlName="is_inbox_tag">
<label class="form-check-label" for="is_inbox_tag">Inbox Tag</label>
</div>
<div class="form-group form-check">
<input type="checkbox" class="form-check-input" id="automatic_classification"
formControlName="automatic_classification">
<label class="form-check-label" for="automatic_classification">Automatic Classification</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>

View File

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

View File

@@ -0,0 +1,37 @@
import { Component } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { TagService } from 'src/app/services/rest/tag.service';
import { ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-tag-edit-dialog',
templateUrl: './tag-edit-dialog.component.html',
styleUrls: ['./tag-edit-dialog.component.css']
})
export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> {
constructor(service: TagService, activeModal: NgbActiveModal, toastService: ToastService) {
super(service, activeModal, toastService, 'tag')
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(''),
colour: new FormControl(1),
is_inbox_tag: new FormControl(false),
automatic_classification: new FormControl(true)
})
}
getColours() {
return PaperlessTag.COLOURS
}
getColor(id: number) {
return PaperlessTag.COLOURS.find(c => c.id == id)
}
}

View File

@@ -0,0 +1,34 @@
<app-page-header title="Tags">
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="openCreateDialog()">
Create
</button>
</app-page-header>
<ngb-pagination [pageSize]="25" [collectionSize]="collectionSize" [(page)]="page" (pageChange)="reloadData()" aria-label="Default pagination"></ngb-pagination>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Colour</th>
<th scope="col">Automatic Classification</th>
<th scope="col">Document count</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let tag of data">
<td scope="row">{{ tag.name }}</td>
<td scope="row"><span class="badge" [style.color]="getColor(tag.colour).textColor" [style.background-color]="getColor(tag.colour).value">{{ getColor(tag.colour).name }}</span></td>
<td scope="row">{{ tag.automatic_classification }}</td>
<td scope="row">{{ tag.document_count }}</td>
<td scope="row">
<div class="btn-group">
<button class="btn btn-sm btn-outline-secondary" (click)="openEditDialog(tag)">Edit</button>
<button class="btn btn-sm btn-outline-danger" (click)="openDeleteDialog(tag)">Delete</button>
</div>
</td>
</tr>
</tbody>
</table>

View File

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

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { 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 { GenericListComponent } from '../generic-list/generic-list.component';
import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component';
@Component({
selector: 'app-tag-list',
templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.css']
})
export class TagListComponent extends GenericListComponent<PaperlessTag> {
constructor(tagService: TagService, modalService: NgbModal) {
super(tagService, modalService, TagEditDialogComponent)
}
getColor(id) {
return PaperlessTag.COLOURS.find(c => c.id == id)
}
}

View File

@@ -0,0 +1,8 @@
<div class="jumbotron text-center">
<svg width="5em" height="7em" viewBox="0 0 16 16" class="bi bi-emoji-frown" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path fill-rule="evenodd" d="M4.285 12.433a.5.5 0 0 0 .683-.183A3.498 3.498 0 0 1 8 10.5c1.295 0 2.426.703 3.032 1.75a.5.5 0 0 0 .866-.5A4.498 4.498 0 0 0 8 9.5a4.5 4.5 0 0 0-3.898 2.25.5.5 0 0 0 .183.683z"/>
<path d="M7 6.5C7 7.328 6.552 8 6 8s-1-.672-1-1.5S5.448 5 6 5s1 .672 1 1.5zm4 0c0 .828-.448 1.5-1 1.5s-1-.672-1-1.5S9.448 5 10 5s1 .672 1 1.5z"/>
</svg>
<h1>404 Not Found</h1>
</div>

View File

@@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NotFoundComponent } from './not-found.component';
describe('NotFoundComponent', () => {
let component: NotFoundComponent;
let fixture: ComponentFixture<NotFoundComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NotFoundComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NotFoundComponent);
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-not-found',
templateUrl: './not-found.component.html',
styleUrls: ['./not-found.component.css']
})
export class NotFoundComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,4 @@
.match {
color: black;
background-color: orange;
}

View File

@@ -0,0 +1,3 @@
... <span *ngFor="let fragment of highlights">
<span *ngFor="let token of fragment" [ngClass]="token.term != null ? 'match term'+ token.term : ''">{{token.text}}</span> ...
</span>

View File

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

View File

@@ -0,0 +1,19 @@
import { Component, Input, OnInit } from '@angular/core';
import { SearchResultHighlightedText } from 'src/app/services/rest/search.service';
@Component({
selector: 'app-result-hightlight',
templateUrl: './result-hightlight.component.html',
styleUrls: ['./result-hightlight.component.css']
})
export class ResultHightlightComponent implements OnInit {
constructor() { }
@Input()
highlights: SearchResultHighlightedText[][]
ngOnInit(): void {
}
}

View File

@@ -0,0 +1,11 @@
.result-content {
color: darkgray;
}
.doc-img {
object-fit: cover;
object-position: top;
height: 100%;
position: absolute;
}

View File

@@ -0,0 +1,12 @@
<app-page-header title="Search results">
</app-page-header>
<p>Search string: <i>{{query}}</i></p>
<app-document-card-large *ngFor="let result of results"
[document]="result.document"
[details]="result.highlights">
</app-document-card-large>
<p *ngIf="results.length == 0" class="mx-auto">No results</p>

Some files were not shown because too many files have changed in this diff Show More