mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-07-28 18:24:38 -05:00
Merge branch 'dev' into celery-tasks
This commit is contained in:
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -12,25 +11,23 @@ import { SettingsComponent } from './components/manage/settings/settings.compone
|
||||
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/:id', component: DocumentListComponent, canActivate: [AuthGuardService] },
|
||||
{path: 'search', component: SearchComponent, canActivate: [AuthGuardService] },
|
||||
{path: 'documents/:id', component: DocumentDetailComponent, canActivate: [AuthGuardService] },
|
||||
{path: 'dashboard', component: DashboardComponent },
|
||||
{path: 'documents', component: DocumentListComponent },
|
||||
{path: 'view/:id', component: DocumentListComponent },
|
||||
{path: 'search', component: SearchComponent },
|
||||
{path: 'documents/:id', component: DocumentDetailComponent },
|
||||
|
||||
{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: 'tags', component: TagListComponent },
|
||||
{path: 'documenttypes', component: DocumentTypeListComponent },
|
||||
{path: 'correspondents', component: CorrespondentListComponent },
|
||||
{path: 'logs', component: LogsComponent },
|
||||
{path: 'settings', component: SettingsComponent },
|
||||
]},
|
||||
|
||||
{path: 'login', component: LoginComponent },
|
||||
{path: '404', component: NotFoundComponent},
|
||||
{path: '**', redirectTo: '/404', pathMatch: 'full'}
|
||||
];
|
||||
|
@@ -12,7 +12,6 @@ import { TagListComponent } from './components/manage/tag-list/tag-list.componen
|
||||
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';
|
||||
@@ -29,7 +28,6 @@ import { PageHeaderComponent } from './components/common/page-header/page-header
|
||||
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';
|
||||
import { NgxFileDropModule } from 'ngx-file-drop';
|
||||
@@ -40,6 +38,7 @@ import { SaveViewConfigDialogComponent } from './components/document-list/save-v
|
||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||
import { DateTimeComponent } from './components/common/input/date-time/date-time.component';
|
||||
import { TagsComponent } from './components/common/input/tags/tags.component';
|
||||
import { SortableDirective } from './directives/sortable.directive';
|
||||
import { SavedViewWidgetComponent } from './components/dashboard/widgets/saved-view-widget/saved-view-widget.component';
|
||||
import { ConsumerStatusWidgetComponent } from './components/dashboard/widgets/consumer-status-widget/consumer-status-widget.component';
|
||||
import { StatisticsWidgetComponent } from './components/dashboard/widgets/statistics-widget/statistics-widget.component';
|
||||
@@ -56,7 +55,6 @@ import { FileUploadWidgetComponent } from './components/dashboard/widgets/file-u
|
||||
DocumentTypeListComponent,
|
||||
LogsComponent,
|
||||
SettingsComponent,
|
||||
LoginComponent,
|
||||
SafePipe,
|
||||
NotFoundComponent,
|
||||
CorrespondentEditDialogComponent,
|
||||
@@ -78,6 +76,7 @@ import { FileUploadWidgetComponent } from './components/dashboard/widgets/file-u
|
||||
SaveViewConfigDialogComponent,
|
||||
DateTimeComponent,
|
||||
TagsComponent,
|
||||
SortableDirective,
|
||||
ConsumerStatusWidgetComponent,
|
||||
SavedViewWidgetComponent,
|
||||
StatisticsWidgetComponent,
|
||||
@@ -94,12 +93,7 @@ import { FileUploadWidgetComponent } from './components/dashboard/widgets/file-u
|
||||
InfiniteScrollModule
|
||||
],
|
||||
providers: [
|
||||
DatePipe,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
}
|
||||
DatePipe
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
|
@@ -10,7 +10,7 @@
|
||||
</form>
|
||||
<ul class="navbar-nav px-3">
|
||||
<li class="nav-item text-nowrap">
|
||||
<a class="nav-link" (click)="logout()" style="cursor: pointer;">
|
||||
<a class="nav-link" href="accounts/logout/">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#door-closed"/>
|
||||
</svg>
|
||||
|
@@ -1,10 +1,9 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { from, Observable, of, scheduled, Subscription } from 'rxjs';
|
||||
import { from, Observable, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
|
||||
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';
|
||||
import { SearchService } from 'src/app/services/rest/search.service';
|
||||
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
|
||||
@@ -19,7 +18,6 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
||||
constructor (
|
||||
public router: Router,
|
||||
private openDocumentsService: OpenDocumentsService,
|
||||
private authService: AuthService,
|
||||
private searchService: SearchService,
|
||||
public viewConfigService: SavedViewConfigService
|
||||
) {
|
||||
@@ -64,10 +62,6 @@ export class AppFrameComponent implements OnInit, OnDestroy {
|
||||
this.router.navigate(['search'], {queryParams: {query: this.searchField.value}})
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.authService.logout()
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.openDocuments = this.openDocumentsService.getOpenDocuments()
|
||||
}
|
||||
|
@@ -134,8 +134,8 @@ export class DocumentDetailComponent implements OnInit {
|
||||
|
||||
close() {
|
||||
this.openDocumentService.closeDocument(this.document)
|
||||
if (this.documentListViewService.viewConfig) {
|
||||
this.router.navigate(['view', this.documentListViewService.viewConfig.id])
|
||||
if (this.documentListViewService.viewId) {
|
||||
this.router.navigate(['view', this.documentListViewService.viewId])
|
||||
} else {
|
||||
this.router.navigate(['documents'])
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<app-page-header [title]="docs.viewConfig ? docs.viewConfig.title : 'Documents'">
|
||||
<app-page-header [title]="getTitle()">
|
||||
|
||||
<div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="displayMode"
|
||||
(ngModelChange)="saveDisplayMode()">
|
||||
@@ -21,14 +21,13 @@
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="docs.currentSortDirection"
|
||||
(ngModelChange)="reload()"
|
||||
*ngIf="!docs.viewConfig">
|
||||
<div class="btn-group btn-group-toggle mr-2" ngbRadioGroup [(ngModel)]="docs.sortDirection"
|
||||
*ngIf="!docs.viewId">
|
||||
<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)"
|
||||
[class.active]="docs.currentSortField == f.field">{{f.name}}</button>
|
||||
[class.active]="docs.sortField == f.field">{{f.name}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<label ngbButtonLabel class="btn-outline-secondary btn-sm">
|
||||
@@ -44,7 +43,7 @@
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
<div class="btn-group" *ngIf="!docs.viewConfig">
|
||||
<div class="btn-group" *ngIf="!docs.viewId">
|
||||
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="showFilter=!showFilter">
|
||||
<svg class="toolbaricon" fill="currentColor">
|
||||
@@ -62,7 +61,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</app-page-header>
|
||||
|
||||
|
@@ -26,13 +26,16 @@ export class DocumentListComponent implements OnInit {
|
||||
filterRules: FilterRule[] = []
|
||||
showFilter = false
|
||||
|
||||
getTitle() {
|
||||
return this.docs.viewConfigOverride ? this.docs.viewConfigOverride.title : "Documents"
|
||||
}
|
||||
|
||||
getSortFields() {
|
||||
return DOCUMENT_SORT_FIELDS
|
||||
}
|
||||
|
||||
setSort(field: string) {
|
||||
this.docs.currentSortField = field
|
||||
this.reload()
|
||||
this.docs.sortField = field
|
||||
}
|
||||
|
||||
saveDisplayMode() {
|
||||
@@ -45,11 +48,11 @@ export class DocumentListComponent implements OnInit {
|
||||
}
|
||||
this.route.paramMap.subscribe(params => {
|
||||
if (params.has('id')) {
|
||||
this.docs.viewConfig = this.savedViewConfigService.getConfig(params.get('id'))
|
||||
this.docs.viewConfigOverride = this.savedViewConfigService.getConfig(params.get('id'))
|
||||
} else {
|
||||
this.filterRules = cloneFilterRules(this.docs.currentFilterRules)
|
||||
this.filterRules = this.docs.filterRules
|
||||
this.showFilter = this.filterRules.length > 0
|
||||
this.docs.viewConfig = null
|
||||
this.docs.viewConfigOverride = null
|
||||
}
|
||||
this.reload()
|
||||
})
|
||||
@@ -60,28 +63,24 @@ export class DocumentListComponent implements OnInit {
|
||||
}
|
||||
|
||||
applyFilterRules() {
|
||||
this.docs.setFilterRules(this.filterRules)
|
||||
this.reload()
|
||||
this.docs.filterRules = this.filterRules
|
||||
}
|
||||
|
||||
loadViewConfig(config: SavedViewConfig) {
|
||||
this.filterRules = cloneFilterRules(config.filterRules)
|
||||
this.docs.setFilterRules(config.filterRules)
|
||||
this.docs.currentSortField = config.sortField
|
||||
this.docs.currentSortDirection = config.sortDirection
|
||||
this.reload()
|
||||
this.docs.loadViewConfig(config)
|
||||
}
|
||||
|
||||
saveViewConfig() {
|
||||
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
|
||||
modal.componentInstance.saveClicked.subscribe(formValue => {
|
||||
this.savedViewConfigService.saveConfig({
|
||||
filterRules: cloneFilterRules(this.filterRules),
|
||||
title: formValue.title,
|
||||
showInDashboard: formValue.showInDashboard,
|
||||
showInSideBar: formValue.showInSideBar,
|
||||
sortDirection: this.docs.currentSortDirection,
|
||||
sortField: this.docs.currentSortField
|
||||
filterRules: this.docs.filterRules,
|
||||
sortDirection: this.docs.sortDirection,
|
||||
sortField: this.docs.sortField
|
||||
})
|
||||
modal.close()
|
||||
})
|
||||
|
@@ -1,44 +0,0 @@
|
||||
.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;
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
<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>
|
@@ -1,25 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
@@ -1,34 +0,0 @@
|
||||
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 { 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.showError("Unable to log in with provided credentials.")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -9,10 +9,10 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Matching</th>
|
||||
<th scope="col">Document count</th>
|
||||
<th scope="col">Last correspondence</th>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)">Last correspondence</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@@ -10,9 +10,9 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Matching</th>
|
||||
<th scope="col">Document count</th>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@@ -1,7 +1,8 @@
|
||||
import { Directive, OnInit } from '@angular/core';
|
||||
import { Directive, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||
import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
|
||||
|
||||
@@ -14,12 +15,17 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
||||
private editDialogComponent: any) {
|
||||
}
|
||||
|
||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
||||
|
||||
public data: T[] = []
|
||||
|
||||
public page = 1
|
||||
|
||||
public collectionSize = 0
|
||||
|
||||
public sortField: string
|
||||
public sortDirection: string
|
||||
|
||||
getMatching(o: MatchingModel) {
|
||||
if (o.matching_algorithm == MATCH_AUTO) {
|
||||
return "Automatic"
|
||||
@@ -30,12 +36,31 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
||||
}
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
|
||||
if (event.direction && event.direction.length > 0) {
|
||||
this.sortField = event.column
|
||||
this.sortDirection = event.direction
|
||||
} else {
|
||||
this.sortField = null
|
||||
this.sortDirection = null
|
||||
}
|
||||
|
||||
this.headers.forEach(header => {
|
||||
if (header.sortable !== this.sortField) {
|
||||
header.direction = '';
|
||||
}
|
||||
});
|
||||
|
||||
this.reloadData()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.reloadData()
|
||||
}
|
||||
|
||||
reloadData() {
|
||||
this.service.list(this.page).subscribe(c => {
|
||||
this.service.list(this.page, null, this.sortField, this.sortDirection).subscribe(c => {
|
||||
this.data = c.results
|
||||
this.collectionSize = c.count
|
||||
});
|
||||
|
@@ -20,7 +20,7 @@ export class LogsComponent implements OnInit {
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.logService.list(1, 50, null, {'level__gte': this.level}).subscribe(result => this.logs = result.results)
|
||||
this.logService.list(1, 50, 'created', 'des', {'level__gte': this.level}).subscribe(result => this.logs = result.results)
|
||||
}
|
||||
|
||||
getLevelText(level: number) {
|
||||
@@ -32,7 +32,7 @@ export class LogsComponent implements OnInit {
|
||||
if (this.logs.length > 0) {
|
||||
lastCreated = this.logs[this.logs.length-1].created
|
||||
}
|
||||
this.logService.list(1, 25, null, {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
|
||||
this.logService.list(1, 25, 'created', 'des', {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
|
||||
this.logs.push(...result.results)
|
||||
})
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@
|
||||
<a ngbNavLink>Saved views</a>
|
||||
<ng-template ngbNavContent>
|
||||
|
||||
<table class="table table-striped">
|
||||
<table class="table table-borderless table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Title</th>
|
||||
@@ -57,7 +57,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3"></div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Save</button>
|
||||
</form>
|
@@ -9,10 +9,10 @@
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||
<th scope="col">Colour</th>
|
||||
<th scope="col">Matching</th>
|
||||
<th scope="col">Document count</th>
|
||||
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||
<th scope="col">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@@ -10,10 +10,10 @@ export interface SavedViewConfig {
|
||||
|
||||
sortDirection: string
|
||||
|
||||
title: string
|
||||
title?: string
|
||||
|
||||
showInSideBar: boolean
|
||||
showInSideBar?: boolean
|
||||
|
||||
showInDashboard: boolean
|
||||
showInDashboard?: boolean
|
||||
|
||||
}
|
@@ -2,6 +2,10 @@ export const OPEN_DOCUMENT_SERVICE = {
|
||||
DOCUMENTS: 'open-documents-service:openDocuments'
|
||||
}
|
||||
|
||||
export const DOCUMENT_LIST_SERVICE = {
|
||||
CURRENT_VIEW_CONFIG: 'document-list-service:currentViewConfig'
|
||||
}
|
||||
|
||||
export const GENERAL_SETTINGS = {
|
||||
DOCUMENT_LIST_SIZE: 'general-settings:documentListSize',
|
||||
DOCUMENT_LIST_SIZE_DEFAULT: 50
|
||||
|
8
src-ui/src/app/directives/sortable.directive.spec.ts
Normal file
8
src-ui/src/app/directives/sortable.directive.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { SortableDirective } from './sortable.directive';
|
||||
|
||||
describe('SortableDirective', () => {
|
||||
it('should create an instance', () => {
|
||||
const directive = new SortableDirective();
|
||||
expect(directive).toBeTruthy();
|
||||
});
|
||||
});
|
30
src-ui/src/app/directives/sortable.directive.ts
Normal file
30
src-ui/src/app/directives/sortable.directive.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Directive, EventEmitter, Input, Output } from '@angular/core';
|
||||
|
||||
export interface SortEvent {
|
||||
column: string;
|
||||
direction: string;
|
||||
}
|
||||
|
||||
const rotate: {[key: string]: string} = { 'asc': 'des', 'des': '', '': 'asc' };
|
||||
|
||||
@Directive({
|
||||
selector: 'th[sortable]',
|
||||
host: {
|
||||
'[class.asc]': 'direction === "asc"',
|
||||
'[class.des]': 'direction === "des"',
|
||||
'(click)': 'rotate()'
|
||||
}
|
||||
})
|
||||
export class SortableDirective {
|
||||
|
||||
constructor() { }
|
||||
|
||||
@Input() sortable: string = '';
|
||||
@Input() direction: string = '';
|
||||
@Output() sort = new EventEmitter<SortEvent>();
|
||||
|
||||
rotate() {
|
||||
this.direction = rotate[this.direction];
|
||||
this.sort.emit({column: this.sortable, direction: this.direction});
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthGuardService } from './auth-guard.service';
|
||||
|
||||
describe('AuthGuardService', () => {
|
||||
let service: AuthGuardService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthGuardService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuardService {
|
||||
|
||||
constructor(public auth: AuthService, public router: Router) { }
|
||||
|
||||
canActivate(): boolean {
|
||||
if (!this.auth.isAuthenticated()) {
|
||||
this.router.navigate(['login']);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthInterceptor } from './auth.interceptor';
|
||||
|
||||
describe('AuthInterceptor', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({
|
||||
providers: [
|
||||
AuthInterceptor
|
||||
]
|
||||
}));
|
||||
|
||||
it('should be created', () => {
|
||||
const interceptor: AuthInterceptor = TestBed.inject(AuthInterceptor);
|
||||
expect(interceptor).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,37 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
HttpRequest,
|
||||
HttpHandler,
|
||||
HttpEvent,
|
||||
HttpInterceptor,
|
||||
HttpErrorResponse
|
||||
} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { Toast, ToastService } from './toast.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private authService: AuthService, private toastService: ToastService) {}
|
||||
|
||||
intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
|
||||
if (this.authService.isAuthenticated()) {
|
||||
request = request.clone({
|
||||
setHeaders: {
|
||||
Authorization: 'Token ' + this.authService.getToken()
|
||||
}
|
||||
});
|
||||
}
|
||||
return next.handle(request).pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (error.status == 401 && this.authService.isAuthenticated()) {
|
||||
this.authService.logout()
|
||||
this.toastService.showError("Your session has expired. Please log in again.")
|
||||
}
|
||||
return throwError(error)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
let service: AuthService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(AuthService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
@@ -1,72 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { map } from 'rxjs/operators';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Router } from '@angular/router';
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
interface TokenResponse {
|
||||
token: string
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
|
||||
private currentUsername: string
|
||||
|
||||
private token: string
|
||||
|
||||
constructor(private http: HttpClient, private router: Router) {
|
||||
this.token = localStorage.getItem('auth-service:token')
|
||||
if (this.token == null) {
|
||||
this.token = sessionStorage.getItem('auth-service:token')
|
||||
}
|
||||
this.currentUsername = localStorage.getItem('auth-service:currentUsername')
|
||||
if (this.currentUsername == null) {
|
||||
this.currentUsername = sessionStorage.getItem('auth-service:currentUsername')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private requestToken(username: string, password: string): Observable<TokenResponse> {
|
||||
return this.http.post<TokenResponse>(`${environment.apiBaseUrl}token/`, {"username": username, "password": password})
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.currentUsername != null
|
||||
}
|
||||
|
||||
logout() {
|
||||
this.currentUsername = null
|
||||
this.token = null
|
||||
localStorage.removeItem('auth-service:token')
|
||||
localStorage.removeItem('auth-service:currentUsername')
|
||||
sessionStorage.removeItem('auth-service:token')
|
||||
sessionStorage.removeItem('auth-service:currentUsername')
|
||||
this.router.navigate(['login'])
|
||||
}
|
||||
|
||||
login(username: string, password: string, rememberMe: boolean): Observable<boolean> {
|
||||
return this.requestToken(username,password).pipe(
|
||||
map(tokenResponse => {
|
||||
this.currentUsername = username
|
||||
this.token = tokenResponse.token
|
||||
let storage = rememberMe ? localStorage : sessionStorage
|
||||
storage.setItem('auth-service:token', this.token)
|
||||
storage.setItem('auth-service:currentUsername', this.currentUsername)
|
||||
return true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
getToken(): string {
|
||||
return this.token
|
||||
}
|
||||
|
||||
getCurrentUsername(): string {
|
||||
return this.currentUsername
|
||||
}
|
||||
}
|
@@ -3,8 +3,8 @@ import { Observable } from 'rxjs';
|
||||
import { cloneFilterRules, FilterRule } from '../data/filter-rule';
|
||||
import { PaperlessDocument } from '../data/paperless-document';
|
||||
import { SavedViewConfig } from '../data/saved-view-config';
|
||||
import { GENERAL_SETTINGS } from '../data/storage-keys';
|
||||
import { DocumentService, SORT_DIRECTION_DESCENDING } from './rest/document.service';
|
||||
import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys';
|
||||
import { DocumentService } from './rest/document.service';
|
||||
|
||||
|
||||
@Injectable({
|
||||
@@ -18,33 +18,24 @@ export class DocumentListViewService {
|
||||
currentPage = 1
|
||||
currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
|
||||
collectionSize: number
|
||||
|
||||
currentFilterRules: FilterRule[] = []
|
||||
currentSortDirection = SORT_DIRECTION_DESCENDING
|
||||
currentSortField = DocumentListViewService.DEFAULT_SORT_FIELD
|
||||
|
||||
viewConfig: SavedViewConfig
|
||||
private currentViewConfig: SavedViewConfig
|
||||
//TODO: make private
|
||||
viewConfigOverride: SavedViewConfig
|
||||
|
||||
get viewId() {
|
||||
return this.viewConfigOverride?.id
|
||||
}
|
||||
|
||||
reload(onFinish?) {
|
||||
let sortField: string
|
||||
let sortDirection: string
|
||||
let filterRules: FilterRule[]
|
||||
if (this.viewConfig) {
|
||||
sortField = this.viewConfig.sortField
|
||||
sortDirection = this.viewConfig.sortDirection
|
||||
filterRules = this.viewConfig.filterRules
|
||||
} else {
|
||||
sortField = this.currentSortField
|
||||
sortDirection = this.currentSortDirection
|
||||
filterRules = this.currentFilterRules
|
||||
}
|
||||
let viewConfig = this.viewConfigOverride || this.currentViewConfig
|
||||
|
||||
this.documentService.list(
|
||||
this.currentPage,
|
||||
this.currentPageSize,
|
||||
sortField,
|
||||
sortDirection,
|
||||
filterRules).subscribe(
|
||||
viewConfig.sortField,
|
||||
viewConfig.sortDirection,
|
||||
viewConfig.filterRules).subscribe(
|
||||
result => {
|
||||
this.collectionSize = result.count
|
||||
this.documents = result.results
|
||||
@@ -60,9 +51,43 @@ export class DocumentListViewService {
|
||||
})
|
||||
}
|
||||
|
||||
set filterRules(filterRules: FilterRule[]) {
|
||||
this.currentViewConfig.filterRules = cloneFilterRules(filterRules)
|
||||
this.saveCurrentViewConfig()
|
||||
this.reload()
|
||||
}
|
||||
|
||||
setFilterRules(filterRules: FilterRule[]) {
|
||||
this.currentFilterRules = cloneFilterRules(filterRules)
|
||||
get filterRules(): FilterRule[] {
|
||||
return cloneFilterRules(this.currentViewConfig.filterRules)
|
||||
}
|
||||
|
||||
set sortField(field: string) {
|
||||
this.currentViewConfig.sortField = field
|
||||
this.saveCurrentViewConfig()
|
||||
this.reload()
|
||||
}
|
||||
|
||||
get sortField(): string {
|
||||
return this.currentViewConfig.sortField
|
||||
}
|
||||
|
||||
set sortDirection(direction: string) {
|
||||
this.currentViewConfig.sortDirection = direction
|
||||
this.saveCurrentViewConfig()
|
||||
this.reload()
|
||||
}
|
||||
|
||||
get sortDirection(): string {
|
||||
return this.currentViewConfig.sortDirection
|
||||
}
|
||||
|
||||
loadViewConfig(config: SavedViewConfig) {
|
||||
Object.assign(this.currentViewConfig, config)
|
||||
this.reload()
|
||||
}
|
||||
|
||||
private saveCurrentViewConfig() {
|
||||
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.currentViewConfig))
|
||||
}
|
||||
|
||||
getLastPage(): number {
|
||||
@@ -108,5 +133,22 @@ export class DocumentListViewService {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private documentService: DocumentService) { }
|
||||
constructor(private documentService: DocumentService) {
|
||||
let currentViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
if (currentViewConfigJson) {
|
||||
try {
|
||||
this.currentViewConfig = JSON.parse(currentViewConfigJson)
|
||||
} catch (e) {
|
||||
sessionStorage.removeItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
|
||||
this.currentViewConfig = null
|
||||
}
|
||||
}
|
||||
if (!this.currentViewConfig) {
|
||||
this.currentViewConfig = {
|
||||
filterRules: [],
|
||||
sortDirection: 'des',
|
||||
sortField: 'created'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,17 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
||||
return url
|
||||
}
|
||||
|
||||
list(page?: number, pageSize?: number, ordering?: string, extraParams?): Observable<Results<T>> {
|
||||
private getOrderingQueryParam(sortField: string, sortDirection: string) {
|
||||
if (sortField && sortDirection) {
|
||||
return (sortDirection == 'des' ? '-' : '') + sortField
|
||||
} else if (sortField) {
|
||||
return sortField
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> {
|
||||
let httpParams = new HttpParams()
|
||||
if (page) {
|
||||
httpParams = httpParams.set('page', page.toString())
|
||||
@@ -29,6 +39,7 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
||||
if (pageSize) {
|
||||
httpParams = httpParams.set('page_size', pageSize.toString())
|
||||
}
|
||||
let ordering = this.getOrderingQueryParam(sortField, sortDirection)
|
||||
if (ordering) {
|
||||
httpParams = httpParams.set('ordering', ordering)
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document';
|
||||
import { AbstractPaperlessService } from './abstract-paperless-service';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Results } from 'src/app/data/results';
|
||||
import { FilterRule } from 'src/app/data/filter-rule';
|
||||
@@ -10,6 +9,7 @@ import { FilterRule } from 'src/app/data/filter-rule';
|
||||
|
||||
export const DOCUMENT_SORT_FIELDS = [
|
||||
{ field: "correspondent__name", name: "Correspondent" },
|
||||
{ field: "document_type__name", name: "Document type" },
|
||||
{ field: 'title', name: 'Title' },
|
||||
{ field: 'archive_serial_number', name: 'ASN' },
|
||||
{ field: 'created', name: 'Created' },
|
||||
@@ -26,7 +26,7 @@ export const SORT_DIRECTION_DESCENDING = "des"
|
||||
})
|
||||
export class DocumentService extends AbstractPaperlessService<PaperlessDocument> {
|
||||
|
||||
constructor(http: HttpClient, private auth: AuthService) {
|
||||
constructor(http: HttpClient) {
|
||||
super(http, 'documents')
|
||||
}
|
||||
|
||||
@@ -46,28 +46,20 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
||||
}
|
||||
}
|
||||
|
||||
private getOrderingQueryParam(sortField: string, sortDirection: string) {
|
||||
if (DOCUMENT_SORT_FIELDS.find(f => f.field == sortField)) {
|
||||
return (sortDirection == SORT_DIRECTION_DESCENDING ? '-' : '') + sortField
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> {
|
||||
return super.list(page, pageSize, this.getOrderingQueryParam(sortField, sortDirection), this.filterRulesToQueryParams(filterRules))
|
||||
return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules))
|
||||
}
|
||||
|
||||
getPreviewUrl(id: number): string {
|
||||
return this.getResourceUrl(id, 'preview') + `?auth_token=${this.auth.getToken()}`
|
||||
return this.getResourceUrl(id, 'preview')
|
||||
}
|
||||
|
||||
getThumbUrl(id: number): string {
|
||||
return this.getResourceUrl(id, 'thumb') + `?auth_token=${this.auth.getToken()}`
|
||||
return this.getResourceUrl(id, 'thumb')
|
||||
}
|
||||
|
||||
getDownloadUrl(id: number): string {
|
||||
return this.getResourceUrl(id, 'download') + `?auth_token=${this.auth.getToken()}`
|
||||
return this.getResourceUrl(id, 'download')
|
||||
}
|
||||
|
||||
uploadDocument(formData) {
|
||||
|
@@ -10,7 +10,11 @@ export class SavedViewConfigService {
|
||||
constructor() {
|
||||
let savedConfigs = localStorage.getItem('saved-view-config-service:savedConfigs')
|
||||
if (savedConfigs) {
|
||||
this.configs = JSON.parse(savedConfigs)
|
||||
try {
|
||||
this.configs = JSON.parse(savedConfigs)
|
||||
} catch (e) {
|
||||
this.configs = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user