Merge branch 'dev' into feature-bulk-edit

This commit is contained in:
jonaswinkler
2020-12-15 03:13:22 +01:00
68 changed files with 1198 additions and 489 deletions

View File

@@ -27,6 +27,9 @@ 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 { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.component';
import { FilterDropdownButtonComponent } from './components/filter-editor/filter-dropdown/filter-dropdown-button/filter-dropdown-button.component';
import { FilterDropdownDateComponent } from './components/filter-editor/filter-dropdown-date/filter-dropdown-date.component';
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';
@@ -48,6 +51,7 @@ import { PdfViewerModule } from 'ng2-pdf-viewer';
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
import { YesNoPipe } from './pipes/yes-no.pipe';
import { FileSizePipe } from './pipes/file-size.pipe';
import { FilterPipe } from './pipes/filter.pipe';
import { DocumentTitlePipe } from './pipes/document-title.pipe';
import { MetadataCollapseComponent } from './components/document-detail/metadata-collapse/metadata-collapse.component';
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
@@ -75,6 +79,9 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
AppFrameComponent,
ToastsComponent,
FilterEditorComponent,
FilterDropdownComponent,
FilterDropdownButtonComponent,
FilterDropdownDateComponent,
DocumentCardLargeComponent,
DocumentCardSmallComponent,
TextComponent,
@@ -91,6 +98,7 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
WelcomeWidgetComponent,
YesNoPipe,
FileSizePipe,
FilterPipe,
DocumentTitlePipe,
MetadataCollapseComponent,
SelectDialogComponent
@@ -112,7 +120,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
provide: HTTP_INTERCEPTORS,
useClass: CsrfInterceptor,
multi: true
}
},
FilterPipe
],
bootstrap: [AppComponent]
})

View File

@@ -37,16 +37,16 @@
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='viewConfigService.getSideBarConfigs().length > 0'>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted" *ngIf='savedViewService.sidebarViews.length > 0'>
<span>Saved views</span>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item w-100" *ngFor='let config of viewConfigService.getSideBarConfigs()'>
<a class="nav-link text-truncate" routerLink="view/{{config.id}}" routerLinkActive="active" (click)="closeMenu()">
<li class="nav-item w-100" *ngFor="let view of savedViewService.sidebarViews">
<a class="nav-link text-truncate" routerLink="view/{{view.id}}" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel"/>
</svg>
{{config.title}}
{{view.name}}
</a>
</li>
</ul>
@@ -132,7 +132,7 @@
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="https://paperless-ng.readthedocs.io/en/latest/">
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://paperless-ng.readthedocs.io/en/latest/">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#question-circle"/>
</svg>
@@ -140,7 +140,7 @@
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/jonaswinkler/paperless-ng">
<a class="nav-link" target="_blank" rel="noopener noreferrer" href="https://github.com/jonaswinkler/paperless-ng">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#link"/>
</svg>

View File

@@ -5,8 +5,8 @@ import { from, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { SearchService } from 'src/app/services/rest/search.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { DocumentDetailComponent } from '../document-detail/document-detail.component';
@Component({
@@ -21,7 +21,7 @@ export class AppFrameComponent implements OnInit, OnDestroy {
private activatedRoute: ActivatedRoute,
private openDocumentsService: OpenDocumentsService,
private searchService: SearchService,
public viewConfigService: SavedViewConfigService
public savedViewService: SavedViewService
) {
}

View File

@@ -1,5 +1,5 @@
import { Component, Directive, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Directive, Input, OnInit } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid';
@Directive()

View File

@@ -1,7 +1,6 @@
import { formatDate } from '@angular/common';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';
@Component({
providers: [{

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, forwardRef, Input, OnInit, Output } from '@angular/core';
import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { AbstractInputComponent } from '../abstract-input';

View File

@@ -1,8 +1,6 @@
import { ThrowStmt } from '@angular/compiler';
import { Component, forwardRef, Input, OnInit } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { TagEditDialogComponent } from 'src/app/components/manage/tag-list/tag-edit-dialog/tag-edit-dialog.component';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { TagService } from 'src/app/services/rest/tag.service';

View File

@@ -1,21 +1,29 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-page-header',
templateUrl: './page-header.component.html',
styleUrls: ['./page-header.component.scss']
})
export class PageHeaderComponent implements OnInit {
export class PageHeaderComponent {
constructor() { }
constructor(private titleService: Title) { }
_title = ""
@Input()
title: string = ""
set title(title: string) {
this._title = title
this.titleService.setTitle(`${this.title} - ${environment.appTitle}`)
}
get title() {
return this._title
}
@Input()
subTitle: string = ""
ngOnInit(): void {
}
}

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
@Component({

View File

@@ -1,7 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@Component({
@@ -12,15 +11,15 @@ import { environment } from 'src/environments/environment';
export class DashboardComponent implements OnInit {
constructor(
public savedViewConfigService: SavedViewConfigService,
private titleService: Title) { }
private savedViewService: SavedViewService) { }
savedViews = []
savedViews: PaperlessSavedView[] = []
ngOnInit(): void {
this.savedViews = this.savedViewConfigService.getDashboardConfigs()
this.titleService.setTitle(`Dashboard - ${environment.appTitle}`)
this.savedViewService.listAll().subscribe(results => {
this.savedViews = results.results.filter(savedView => savedView.show_on_dashboard)
})
}
}

View File

@@ -1,4 +1,4 @@
<app-widget-frame [title]="savedView.title">
<app-widget-frame [title]="savedView.name">
<a header-buttons [routerLink]="" (click)="showAll()">Show all</a>

View File

@@ -1,7 +1,7 @@
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DocumentService } from 'src/app/services/rest/document.service';
@@ -18,18 +18,18 @@ export class SavedViewWidgetComponent implements OnInit {
private list: DocumentListViewService) { }
@Input()
savedView: SavedViewConfig
savedView: PaperlessSavedView
documents: PaperlessDocument[] = []
ngOnInit(): void {
this.documentService.list(1,10,this.savedView.sortField,this.savedView.sortDirection,this.savedView.filterRules).subscribe(result => {
this.documentService.list(1,10,this.savedView.sort_field, this.savedView.sort_reverse, this.savedView.filter_rules).subscribe(result => {
this.documents = result.results
})
}
showAll() {
if (this.savedView.showInSideBar) {
if (this.savedView.show_in_sidebar) {
this.router.navigate(['view', this.savedView.id])
} else {
this.list.load(this.savedView)

View File

@@ -1,6 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
@@ -12,7 +11,6 @@ import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentService } from 'src/app/services/rest/document.service';
import { environment } from 'src/environments/environment';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
@@ -56,8 +54,7 @@ export class DocumentDetailComponent implements OnInit {
private router: Router,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService,
private documentListViewService: DocumentListViewService,
private titleService: Title) { }
private documentListViewService: DocumentListViewService) { }
getContentType() {
return this.metadata?.has_archive_version ? 'application/pdf' : this.metadata?.original_mime_type
@@ -90,7 +87,6 @@ export class DocumentDetailComponent implements OnInit {
updateComponent(doc: PaperlessDocument) {
this.document = doc
this.titleService.setTitle(`${doc.title} - ${environment.appTitle}`)
this.documentsService.getMetadata(doc.id).subscribe(result => {
this.metadata = result
})

View File

@@ -7,7 +7,7 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title">
<h5 class="card-title">
<ng-container *ngIf="document.correspondent">
<a *ngIf="clickCorrespondent.observers.length ; else nolink" [routerLink]="" title="Filter by correspondent" (click)="clickCorrespondent.emit(document.correspondent)" class="font-weight-bold">{{(document.correspondent$ | async)?.name}}</a>
<ng-template #nolink>{{(document.correspondent$ | async)?.name}}</ng-template>:
@@ -52,4 +52,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentService } from 'src/app/services/rest/document.service';
@Component({

View File

@@ -20,7 +20,7 @@
</div>
</div>
</div>
<div class="card-body p-2">
<p class="card-text">
<ng-container *ngIf="document.correspondent">
@@ -53,7 +53,7 @@
</div>
<small class="text-muted">{{document.created | date}}</small>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,7 +1,6 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { map } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { DocumentService } from 'src/app/services/rest/document.service';
@Component({

View File

@@ -44,7 +44,8 @@
</svg>
</label>
</div>
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortDirection">
<div class="btn-group btn-group-toggle ml-2" ngbRadioGroup [(ngModel)]="list.sortReverse">
<div ngbDropdown class="btn-group">
<button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
<div ngbDropdownMenu aria-labelledby="dropdownBasic1" class="shadow">
@@ -53,48 +54,40 @@
</div>
</div>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" value="asc">
<input ngbButton type="radio" class="btn btn-sm" [value]="false">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-down" />
</svg>
</label>
<label ngbButtonLabel class="btn-outline-primary btn-sm">
<input ngbButton type="radio" class="btn btn-sm" value="des">
<input ngbButton type="radio" class="btn btn-sm" [value]="true">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#sort-alpha-up-alt" />
</svg>
</label>
</div>
<div class="btn-group ml-2">
<button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
<svg class="toolbaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#funnel" />
</svg>
Filter
</button>
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle-split" ngbDropdownToggle></button>
<div class="dropdown-menu" ngbDropdownMenu class="shadow">
<ng-container *ngIf="!list.savedViewId" >
<button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
<div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
<button class="btn btn-sm btn-outline-primary dropdown-toggle" ngbDropdownToggle>Views</button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
<ng-container *ngIf="!list.savedViewId">
<button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view)">{{view.name}}</button>
<div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div>
</ng-container>
<button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
<button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
</div>
</div>
</div>
</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 [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor>
</div>
<div class="w-100 mb-4">
<app-filter-editor [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor>
</div>
<div class="d-flex justify-content-between align-items-center">
@@ -104,7 +97,7 @@
</div>
<div *ngIf="displayMode == 'largeCards'">
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)">
<app-document-card-large *ngFor="let d of list.documents" [document]="d" [details]="d.content" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)">
</app-document-card-large>
</div>
@@ -131,16 +124,16 @@
</td>
<td class="d-none d-md-table-cell">
<ng-container *ngIf="d.correspondent">
<a [routerLink]="" (click)="filterByCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
<a [routerLink]="" (click)="clickCorrespondent(d.correspondent)" title="Filter by correspondent">{{(d.correspondent$ | async)?.name}}</a>
</ng-container>
</td>
<td>
<a routerLink="/documents/{{d.id}}" title="Edit document" style="overflow-wrap: anywhere;">{{d.title | documentTitle}}</a>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="filterByTag(t.id)"></app-tag>
<app-tag [tag]="t" *ngFor="let t of d.tags$ | async" class="ml-1" clickable="true" linkTitle="Filter by tag" (click)="clickTag(t)"></app-tag>
</td>
<td class="d-none d-xl-table-cell">
<ng-container *ngIf="d.document_type">
<a [routerLink]="" (click)="filterByDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
<a [routerLink]="" (click)="clickDocumentType(d.document_type)" title="Filter by document type">{{(d.document_type$ | async)?.name}}</a>
</ng-container>
</td>
<td>
@@ -155,5 +148,5 @@
<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>
<app-document-card-small [selected]="list.isSelected(d)" (selectedChange)="list.setSelected(d, $event)" [document]="d" *ngFor="let d of list.documents" (clickTag)="clickTag($event)" (clickCorrespondent)="clickCorrespondent($event)"></app-document-card-small>
</div>

View File

@@ -1,20 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute } from '@angular/router';
import { Component, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { cloneFilterRules, FilterRule } from 'src/app/data/filter-rule';
import { FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
import { environment } from 'src/environments/environment';
import { FilterEditorComponent } from '../filter-editor/filter-editor.component';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component';
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
@@ -28,20 +25,20 @@ export class DocumentListComponent implements OnInit {
constructor(
public list: DocumentListViewService,
public savedViewConfigService: SavedViewConfigService,
public savedViewService: SavedViewService,
public route: ActivatedRoute,
private router: Router,
private toastService: ToastService,
public modalService: NgbModal,
private titleService: Title,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private documentService: DocumentService) { }
displayMode = 'smallCards' // largeCards, smallCards, details
@ViewChild("filterEditor")
private filterEditor: FilterEditorComponent
filterRules: FilterRule[] = []
showFilter = false
displayMode = 'smallCards' // largeCards, smallCards, details
get isFiltered() {
return this.list.filterRules?.length > 0
@@ -64,93 +61,65 @@ export class DocumentListComponent implements OnInit {
this.displayMode = localStorage.getItem('document-list:displayMode')
}
this.route.paramMap.subscribe(params => {
this.list.clear()
if (params.has('id')) {
this.list.savedView = this.savedViewConfigService.getConfig(params.get('id'))
this.filterRules = this.list.filterRules
this.showFilter = false
this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`)
this.savedViewService.getCached(+params.get('id')).subscribe(view => {
if (!view) {
this.router.navigate(["404"])
return
}
this.list.savedView = view
this.list.reload()
})
} else {
this.list.savedView = null
this.filterRules = this.list.filterRules
this.showFilter = this.filterRules.length > 0
this.titleService.setTitle(`Documents - ${environment.appTitle}`)
this.list.reload()
}
this.list.clear()
this.list.reload()
})
}
applyFilterRules() {
this.list.filterRules = this.filterRules
}
clearFilterRules() {
this.list.filterRules = this.filterRules
this.showFilter = false
}
loadViewConfig(config: SavedViewConfig) {
this.filterRules = cloneFilterRules(config.filterRules)
this.list.load(config)
loadViewConfig(view: PaperlessSavedView) {
this.list.load(view)
this.list.reload()
}
saveViewConfig() {
this.savedViewConfigService.updateConfig(this.list.savedView)
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.title}" saved successfully.`))
this.savedViewService.update(this.list.savedView).subscribe(result => {
this.toastService.showToast(Toast.make("Information", `View "${this.list.savedView.name}" saved successfully.`))
})
}
saveViewConfigAs() {
let modal = this.modalService.open(SaveViewConfigDialogComponent, {backdrop: 'static'})
modal.componentInstance.saveClicked.subscribe(formValue => {
this.savedViewConfigService.newConfig({
title: formValue.title,
showInDashboard: formValue.showInDashboard,
showInSideBar: formValue.showInSideBar,
filterRules: this.list.filterRules,
sortDirection: this.list.sortDirection,
sortField: this.list.sortField
let savedView = {
name: formValue.name,
show_on_dashboard: formValue.showOnDashboard,
show_in_sidebar: formValue.showInSideBar,
filter_rules: this.list.filterRules,
sort_reverse: this.list.sortReverse,
sort_field: this.list.sortField
}
this.savedViewService.create(savedView).subscribe(() => {
modal.close()
this.toastService.showToast(Toast.make("Information", `View "${savedView.name}" created successfully.`))
})
modal.close()
})
}
filterByTag(tag_id: number) {
let filterRules = this.list.filterRules
if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) {
return
}
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_HAS_TAG), value: tag_id})
this.filterRules = filterRules
this.applyFilterRules()
clickTag(tagID: number) {
this.filterEditor.toggleTag(tagID)
}
filterByCorrespondent(correspondent_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT)
if (existing_rule && existing_rule.value == correspondent_id) {
return
} else if (existing_rule) {
existing_rule.value = correspondent_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_CORRESPONDENT), value: correspondent_id})
}
this.filterRules = filterRules
this.applyFilterRules()
clickCorrespondent(correspondentID: number) {
this.filterEditor.toggleCorrespondent(correspondentID)
}
filterByDocumentType(document_type_id: number) {
let filterRules = this.list.filterRules
let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE)
if (existing_rule && existing_rule.value == document_type_id) {
return
} else if (existing_rule) {
existing_rule.value = document_type_id
} else {
filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_DOCUMENT_TYPE), value: document_type_id})
}
this.filterRules = filterRules
this.applyFilterRules()
clickDocumentType(documentTypeID: number) {
this.filterEditor.toggleDocumentType(documentTypeID)
}
private executeBulkOperation(method: string, args): Observable<any> {
@@ -265,6 +234,6 @@ export class DocumentListComponent implements OnInit {
modal.close()
}
)
})
})
}
}

View File

@@ -6,9 +6,9 @@
</button>
</div>
<div class="modal-body">
<app-input-text title="Title" formControlName="title"></app-input-text>
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-check title="Show in side bar" formControlName="showInSideBar"></app-input-check>
<app-input-check title="Show in dashboard" formControlName="showInDashboard"></app-input-check>
<app-input-check title="Show on dashboard" formControlName="showOnDashboard"></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>

View File

@@ -15,9 +15,9 @@ export class SaveViewConfigDialogComponent implements OnInit {
public saveClicked = new EventEmitter()
saveViewConfigForm = new FormGroup({
title: new FormControl(''),
name: new FormControl(''),
showInSideBar: new FormControl(false),
showInDashboard: new FormControl(false),
showOnDashboard: new FormControl(false),
})
ngOnInit(): void {

View File

@@ -0,0 +1,43 @@
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="dateBefore || dateAfter ? 'btn-primary' : 'btn-outline-primary'">
{{title}}
</button>
<div class="dropdown-menu date-filter shadow pt-0" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<button class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" (click)="clear()">Clear</button>
<button *ngFor="let range of [7, 30, 'month', 'year']" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(range)">
<ng-container *ngIf="isStringRange(range)">This </ng-container>
{{ range }}
<ng-container *ngIf="!isStringRange(range)"> days</ng-container>
</button>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div>Before</div>
<div class="input-group input-group-sm">
<input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onBeforeSelected($event)" #dpBefore="ngbDatepicker">
<div class="input-group-append">
<button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
<path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/>
</svg>
</button>
</div>
</div>
</div>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div>After</div>
<div class="input-group input-group-sm">
<input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" [maxDate]="this._maxDate" ngbDatepicker (dateSelect)="onAfterSelected($event)" #dpAfter="ngbDatepicker">
<div class="input-group-append">
<button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
<path d="M6.445 11.688V6.354h-.633A12.6 12.6 0 0 0 4.5 7.16v.695c.375-.257.969-.62 1.258-.777h.012v4.61h.675zm1.188-1.305c.047.64.594 1.406 1.703 1.406 1.258 0 2-1.066 2-2.871 0-1.934-.781-2.668-1.953-2.668-.926 0-1.797.672-1.797 1.809 0 1.16.824 1.77 1.676 1.77.746 0 1.23-.376 1.383-.79h.027c-.004 1.316-.461 2.164-1.305 2.164-.664 0-1.008-.45-1.05-.82h-.684zm2.953-2.317c0 .696-.559 1.18-1.184 1.18-.601 0-1.144-.383-1.144-1.2 0-.823.582-1.21 1.168-1.21.633 0 1.16.398 1.16 1.23z"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,7 @@
.date-filter {
min-width: 250px;
.btn-link {
line-height: 1;
}
}

View File

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

View File

@@ -0,0 +1,109 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core';
import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap';
export interface DateSelection {
before?: NgbDateStruct
after?: NgbDateStruct
}
@Component({
selector: 'app-filter-dropdown-date',
templateUrl: './filter-dropdown-date.component.html',
styleUrls: ['./filter-dropdown-date.component.scss']
})
export class FilterDropdownDateComponent {
@Input()
dateBefore: NgbDateStruct
@Input()
dateAfter: NgbDateStruct
@Input()
title: string
@Output()
datesSet = new EventEmitter<DateSelection>()
@ViewChild('dpAfter') dpAfter: NgbDatepicker
@ViewChild('dpBefore') dpBefore: NgbDatepicker
_dateBefore: NgbDateStruct
_dateAfter: NgbDateStruct
get _maxDate(): NgbDate {
let date = new Date()
return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()})
}
isStringRange(range: any) {
return typeof range == 'string'
}
ngOnChanges(changes: SimpleChange) {
// this is a hacky workaround perhaps because of https://github.com/angular/angular/issues/11097
let dateString: string = ''
let dateAfterChange: SimpleChange
let dateBeforeChange: SimpleChange
if (changes) {
dateAfterChange = changes['dateAfter']
dateBeforeChange = changes['dateBefore']
}
if (this.dpBefore && this.dpAfter) {
let dpAfterElRef: ElementRef = this.dpAfter['_elRef']
let dpBeforeElRef: ElementRef = this.dpBefore['_elRef']
if (dateAfterChange && dateAfterChange.currentValue) {
let dateAfterDate = dateAfterChange.currentValue as NgbDateStruct
dateString = `${dateAfterDate.year}-${dateAfterDate.month.toString().padStart(2,'0')}-${dateAfterDate.day.toString().padStart(2,'0')}`
dpAfterElRef.nativeElement.value = dateString
} else if (dateBeforeChange && dateBeforeChange.currentValue) {
let dateBeforeDate = dateBeforeChange.currentValue as NgbDateStruct
dateString = `${dateBeforeDate.year}-${dateBeforeDate.month.toString().padStart(2,'0')}-${dateBeforeDate.day.toString().padStart(2,'0')}`
dpBeforeElRef.nativeElement.value = dateString
} else {
dpAfterElRef.nativeElement.value = dateString
dpBeforeElRef.nativeElement.value = dateString
}
}
}
setDateQuickFilter(range: any) {
this._dateAfter = this._dateBefore = undefined
let date = new Date()
let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() }
switch (typeof range) {
case 'number':
date.setDate(date.getDate() - range)
newDate.year = date.getFullYear()
newDate.month = date.getMonth() + 1
newDate.day = date.getDate()
break
case 'string':
newDate.day = 1
if (range == 'year') newDate.month = 1
break
default:
break
}
this._dateAfter = newDate
this.datesSet.emit({after: newDate, before: null})
}
onBeforeSelected(date: NgbDateStruct) {
this.datesSet.emit({after: this._dateAfter, before: date})
}
onAfterSelected(date: NgbDateStruct) {
this.datesSet.emit({after: date, before: this._dateBefore})
}
clear() {
this.datesSet.emit({after: null, before: null})
}
}

View File

@@ -0,0 +1,12 @@
<button class="list-group-item list-group-item-action d-flex align-items-center p-2 border-top-0 border-left-0 border-right-0 border-bottom" role="menuitem" (click)="toggleItem()">
<div class="selected-icon mr-1">
<svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-check" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z"/>
</svg>
</div>
<div class="mr-1">
<app-tag *ngIf="isTag; else displayName" [tag]="item" [clickable]="true" linkTitle="Filter by tag"></app-tag>
<ng-template #displayName><small>{{item.name}}</small></ng-template>
</div>
<div class="badge badge-light rounded-pill ml-auto mr-1">{{item.document_count}}</div>
</button>

View File

@@ -0,0 +1,4 @@
.selected-icon {
min-width: 1em;
min-height: 1em;
}

View File

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

View File

@@ -0,0 +1,32 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
@Component({
selector: 'app-filter-dropdown-button',
templateUrl: './filter-dropdown-button.component.html',
styleUrls: ['./filter-dropdown-button.component.scss']
})
export class FilterDropdownButtonComponent implements OnInit {
@Input()
item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent
@Input()
selected: boolean
@Output()
toggle = new EventEmitter()
isTag: boolean
ngOnInit() {
this.isTag = 'is_inbox_tag' in this.item // ~ this.item instanceof PaperlessTag
}
toggleItem(): void {
this.selected = !this.selected
this.toggle.emit(this.item)
}
}

View File

@@ -0,0 +1,19 @@
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #filterDropdown="ngbDropdown">
<button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'">
{{title}}
</button>
<div class="dropdown-menu py-0 shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}">
<div class="list-group list-group-flush">
<div class="list-group-item">
<div class="input-group input-group-sm">
<input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput>
</div>
</div>
<div *ngIf="items" class="items">
<ng-container *ngFor="let item of items | filter: filterText; let i = index">
<app-filter-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button>
</ng-container>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,8 @@
.dropdown-menu {
min-width: 250px;
.items {
max-height: 400px;
overflow-y: scroll;
}
}

View File

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

View File

@@ -0,0 +1,58 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
import { ObjectWithId } from 'src/app/data/object-with-id';
import { FilterPipe } from 'src/app/pipes/filter.pipe';
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
@Component({
selector: 'app-filter-dropdown',
templateUrl: './filter-dropdown.component.html',
styleUrls: ['./filter-dropdown.component.scss']
})
export class FilterDropdownComponent {
constructor(private filterPipe: FilterPipe) { }
@Input()
items: ObjectWithId[]
@Input()
itemsSelected: ObjectWithId[]
@Input()
title: string
@Input()
display: string
@Output()
toggle = new EventEmitter()
@ViewChild('listFilterTextInput') listFilterTextInput: ElementRef
@ViewChild('filterDropdown') filterDropdown: NgbDropdown
filterText: string
toggleItem(item: ObjectWithId): void {
this.toggle.emit(item)
}
isItemSelected(item: ObjectWithId): boolean {
return this.itemsSelected?.find(i => i.id == item.id) !== undefined
}
dropdownOpenChange(open: boolean): void {
if (open) {
setTimeout(() => {
this.listFilterTextInput.nativeElement.focus();
}, 0);
} else {
this.filterText = ''
}
}
listFilterEnter(): void {
let filtered = this.filterPipe.transform(this.items, this.filterText)
if (filtered.length == 1) this.toggleItem(filtered.shift())
this.filterDropdown.close()
}
}

View File

@@ -1,52 +1,22 @@
<div *ngFor="let rule of filterRules" class="form-row form-group">
<div class="col-md-3 col-form-label">
<span>{{rule.type.name}}</span>
<div class="form-row form-group mb-0">
<div class="col-auto">
<div class="text-muted mt-1">Filter by:</div>
</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 == 'document_type'" 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)="clearClicked()" class="btn btn-sm btn-outline-secondary">Clear</button>
</div>
<div class="col-auto">
<button (click)="applyClicked()" class="btn btn-sm btn-outline-secondary">Apply</button>
<input class="form-control form-control-sm" type="text" [(ngModel)]="titleFilter" placeholder="Title">
</div>
<app-filter-dropdown class="col-auto" [items]="tags" [itemsSelected]="selectedTags" title="Tags" (toggle)="toggleTag($event.id)"></app-filter-dropdown>
<app-filter-dropdown class="col-auto" [items]="correspondents" [itemsSelected]="selectedCorrespondents" title="Correspondents" (toggle)="toggleCorrespondent($event.id)"></app-filter-dropdown>
<app-filter-dropdown class="col-auto" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" title="Document types" (toggle)="toggleDocumentType($event.id)"></app-filter-dropdown>
<app-filter-dropdown-date class="col-auto" [dateBefore]="dateCreatedBefore" [dateAfter]="dateCreatedAfter" title="Created" (datesSet)="onDatesCreatedSet($event)"></app-filter-dropdown-date>
<app-filter-dropdown-date class="col-auto" [dateBefore]="dateAddedBefore" [dateAfter]="dateAddedAfter" title="Added" (datesSet)="onDatesAddedSet($event)"></app-filter-dropdown-date>
<button class="btn btn-link btn-sm" [disabled]="!hasFilters()" (click)="clearSelected()">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-x" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
Clear all filters
</button>
</div>

View File

@@ -0,0 +1,10 @@
.quick-filter {
min-width: 250px;
max-height: 400px;
overflow-y: scroll;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
}

View File

@@ -1,67 +1,223 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { FilterRule } from 'src/app/data/filter-rule';
import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { PaperlessTag } from 'src/app/data/paperless-tag';
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 { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap';
import { DocumentTypeService } from 'src/app/services/rest/document-type.service';
import { TagService } from 'src/app/services/rest/tag.service';
import { CorrespondentService } from 'src/app/services/rest/correspondent.service';
import { FilterRule } from 'src/app/data/filter-rule';
import { FILTER_ADDED_AFTER, FILTER_ADDED_BEFORE, FILTER_CORRESPONDENT, FILTER_CREATED_AFTER, FILTER_CREATED_BEFORE, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_RULE_TYPES, FILTER_TITLE } from 'src/app/data/filter-rule-type';
import { DateSelection } from './filter-dropdown-date/filter-dropdown-date.component';
@Component({
selector: 'app-filter-editor',
templateUrl: './filter-editor.component.html',
styleUrls: ['./filter-editor.component.scss']
})
export class FilterEditorComponent implements OnInit {
export class FilterEditorComponent implements OnInit, OnDestroy {
constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { }
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
private dateParser: NgbDateParserFormatter
) { }
@Output()
clear = new EventEmitter()
@Input()
filterRules: FilterRule[] = []
@Output()
apply = new EventEmitter()
selectedRuleType: FilterRuleType = FILTER_RULE_TYPES[0]
correspondents: PaperlessCorrespondent[] = []
tags: PaperlessTag[] = []
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[] = []
newRuleClicked() {
this.filterRules.push({type: this.selectedRuleType, value: this.selectedRuleType.default})
this.selectedRuleType = this.getRuleTypes().length > 0 ? this.getRuleTypes()[0] : null
@Input()
filterRules: FilterRule[]
@Output()
filterRulesChange = new EventEmitter<FilterRule[]>()
hasFilters() {
return this.filterRules.length > 0
}
removeRuleClicked(rule) {
let index = this.filterRules.findIndex(r => r == rule)
if (index > -1) {
this.filterRules.splice(index, 1)
}
get selectedTags(): PaperlessTag[] {
let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_HAS_TAG)
return this.tags?.filter(t => tagRules.find(tr => +tr.value == t.id))
}
applyClicked() {
this.apply.next()
get selectedCorrespondents(): PaperlessCorrespondent[] {
let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_CORRESPONDENT)
return this.correspondents?.filter(c => correspondentRules.find(cr => +cr.value == c.id))
}
clearClicked() {
this.filterRules.splice(0,this.filterRules.length)
this.clear.next()
get selectedDocumentTypes(): PaperlessDocumentType[] {
let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.rule_type == FILTER_DOCUMENT_TYPE)
return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => +dtr.value == dt.id))
}
ngOnInit(): void {
this.correspondentService.listAll().subscribe(result => {this.correspondents = result.results})
get titleFilter() {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
return existingRule ? existingRule.value : ''
}
set titleFilter(value) {
this.titleFilterDebounce.next(value)
}
titleFilterDebounce: Subject<string>
subscription: Subscription
ngOnInit() {
this.tagService.listAll().subscribe(result => this.tags = result.results)
this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
this.titleFilterDebounce = new Subject<string>()
this.subscription = this.titleFilterDebounce.pipe(
debounceTime(400),
distinctUntilChanged()
).subscribe(title => {
this.setTitleRule(title)
})
}
getRuleTypes() {
return FILTER_RULE_TYPES.filter(rt => rt.multi || !this.filterRules.find(r => r.type == rt))
ngOnDestroy() {
this.titleFilterDebounce.complete()
// TODO: not sure if both is necessary
this.subscription.unsubscribe()
}
applyFilters() {
this.filterRulesChange.next(this.filterRules)
}
clearSelected() {
this.filterRules = []
this.applyFilters()
}
private toggleFilterRule(filterRuleTypeID: number, value: number) {
let filterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID)
let existingRule = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID && rule.value == value?.toString())
let existingRuleOfSameType = this.filterRules.find(rule => rule.rule_type == filterRuleTypeID)
if (existingRule) {
// if this exact rule already exists, remove it in all cases.
this.filterRules.splice(this.filterRules.indexOf(existingRule), 1)
} else if (filterRuleType.multi || !existingRuleOfSameType) {
// if we allow multiple rules per type, or no rule of this type already exists, push a new rule.
this.filterRules.push({rule_type: filterRuleTypeID, value: value?.toString()})
} else {
// otherwise (i.e., no multi support AND there's already a rule of this type), update the rule.
existingRuleOfSameType.value = value?.toString()
}
this.applyFilters()
}
private setTitleRule(title: string) {
let existingRule = this.filterRules.find(rule => rule.rule_type == FILTER_TITLE)
if (!existingRule && title) {
this.filterRules.push({rule_type: FILTER_TITLE, value: title})
} else if (existingRule && !title) {
this.filterRules.splice(this.filterRules.findIndex(rule => rule.rule_type == FILTER_TITLE), 1)
} else if (existingRule && title) {
existingRule.value = title
}
this.applyFilters()
}
toggleTag(tagId: number) {
this.toggleFilterRule(FILTER_HAS_TAG, tagId)
}
toggleCorrespondent(correspondentId: number) {
this.toggleFilterRule(FILTER_CORRESPONDENT, correspondentId)
}
toggleDocumentType(documentTypeId: number) {
this.toggleFilterRule(FILTER_DOCUMENT_TYPE, documentTypeId)
}
// Date handling
onDatesCreatedSet(dates: DateSelection) {
this.setDateCreatedBefore(dates.before)
this.setDateCreatedAfter(dates.after)
this.applyFilters()
}
onDatesAddedSet(dates: DateSelection) {
this.setDateAddedBefore(dates.before)
this.setDateAddedAfter(dates.after)
this.applyFilters()
}
get dateCreatedBefore(): NgbDateStruct {
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
return createdBeforeRule ? this.dateParser.parse(createdBeforeRule.value) : null
}
get dateCreatedAfter(): NgbDateStruct {
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
return createdAfterRule ? this.dateParser.parse(createdAfterRule.value) : null
}
get dateAddedBefore(): NgbDateStruct {
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
return addedBeforeRule ? this.dateParser.parse(addedBeforeRule.value) : null
}
get dateAddedAfter(): NgbDateStruct {
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
return addedAfterRule ? this.dateParser.parse(addedAfterRule.value) : null
}
setDateCreatedBefore(date?: NgbDateStruct) {
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
else this.clearDateFilter(FILTER_CREATED_BEFORE)
}
setDateCreatedAfter(date?: NgbDateStruct) {
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
else this.clearDateFilter(FILTER_CREATED_AFTER)
}
setDateAddedBefore(date?: NgbDateStruct) {
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
else this.clearDateFilter(FILTER_ADDED_BEFORE)
}
setDateAddedAfter(date?: NgbDateStruct) {
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
else this.clearDateFilter(FILTER_ADDED_AFTER)
}
setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) {
let filterRules = this.filterRules
let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID)
let newValue = this.dateParser.format(date)
if (existingRule) {
existingRule.value = newValue
} else {
filterRules.push({rule_type: dateRuleTypeID, value: newValue})
}
this.filterRules = filterRules
}
clearDateFilter(dateRuleTypeID: number) {
let filterRules = this.filterRules
let existingRule = filterRules.find(rule => rule.rule_type == dateRuleTypeID)
filterRules.splice(filterRules.indexOf(existingRule), 1)
this.filterRules = filterRules
}
}

View File

@@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
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';

View File

@@ -1,9 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
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 { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/correspondent-edit-dialog.component';
@@ -12,9 +10,9 @@ import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog/co
templateUrl: './correspondent-list.component.html',
styleUrls: ['./correspondent-list.component.scss']
})
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> implements OnInit {
export class CorrespondentListComponent extends GenericListComponent<PaperlessCorrespondent> {
constructor(correspondentsService: CorrespondentService, modalService: NgbModal, private titleService: Title) {
constructor(correspondentsService: CorrespondentService, modalService: NgbModal,) {
super(correspondentsService,modalService,CorrespondentEditDialogComponent)
}
@@ -22,9 +20,4 @@ export class CorrespondentListComponent extends GenericListComponent<PaperlessCo
return `correspondent '${object.name}'`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Correspondents - ${environment.appTitle}`)
}
}

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
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';

View File

@@ -1,9 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Component } 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 { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/document-type-edit-dialog.component';
@@ -12,9 +10,9 @@ import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog/doc
templateUrl: './document-type-list.component.html',
styleUrls: ['./document-type-list.component.scss']
})
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> implements OnInit {
export class DocumentTypeListComponent extends GenericListComponent<PaperlessDocumentType> {
constructor(service: DocumentTypeService, modalService: NgbModal, private titleService: Title) {
constructor(service: DocumentTypeService, modalService: NgbModal) {
super(service, modalService, DocumentTypeEditDialogComponent)
}
@@ -22,8 +20,4 @@ export class DocumentTypeListComponent extends GenericListComponent<PaperlessDoc
return `document type '${object.name}'`
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Document types - ${environment.appTitle}`)
}
}

View File

@@ -8,9 +8,9 @@ import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dial
@Directive()
export abstract class GenericListComponent<T extends ObjectWithId> implements OnInit {
constructor(
private service: AbstractPaperlessService<T>,
private service: AbstractPaperlessService<T>,
private modalService: NgbModal,
private editDialogComponent: any) {
}
@@ -60,7 +60,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
}
reloadData() {
this.service.list(this.page, null, this.sortField, this.sortDirection).subscribe(c => {
// TODO: this is a hack
this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => {
this.data = c.results
this.collectionSize = c.count
});

View File

@@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { LOG_LEVELS, LOG_LEVEL_INFO, PaperlessLog } from 'src/app/data/paperless-log';
import { LogService } from 'src/app/services/rest/log.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-logs',
@@ -11,18 +9,17 @@ import { environment } from 'src/environments/environment';
})
export class LogsComponent implements OnInit {
constructor(private logService: LogService, private titleService: Title) { }
constructor(private logService: LogService) { }
logs: PaperlessLog[] = []
level: number = LOG_LEVEL_INFO
ngOnInit(): void {
this.reload()
this.titleService.setTitle(`Logs - ${environment.appTitle}`)
}
reload() {
this.logService.list(1, 50, 'created', 'des', {'level__gte': this.level}).subscribe(result => this.logs = result.results)
this.logService.list(1, 50, 'created', true, {'level__gte': this.level}).subscribe(result => this.logs = result.results)
}
getLevelText(level: number) {
@@ -34,7 +31,7 @@ export class LogsComponent implements OnInit {
if (this.logs.length > 0) {
lastCreated = new Date(this.logs[this.logs.length-1].created).toISOString()
}
this.logService.list(1, 25, 'created', 'des', {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
this.logService.list(1, 25, 'created', true, {'created__lt': lastCreated, 'level__gte': this.level}).subscribe(result => {
this.logs.push(...result.results)
})
}

View File

@@ -34,24 +34,35 @@
<a ngbNavLink>Saved views</a>
<ng-template ngbNavContent>
<table class="table table-borderless table-sm">
<thead>
<tr>
<th scope="col">Title</th>
<th scope="col">Show in dashboard</th>
<th scope="col">Show in sidebar</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let config of savedViewConfigService.getConfigs()">
<td>{{ config.title }}</td>
<td>{{ config.showInDashboard | yesno }}</td>
<td>{{ config.showInSideBar | yesno }}</td>
<td><button type="button" class="btn btn-sm btn-outline-danger" (click)="deleteViewConfig(config)">Delete</button></td>
</tr>
</tbody>
</table>
<div formGroupName="savedViews">
<div *ngFor="let view of savedViews" [formGroupName]="view.id" class="form-row">
<div class="form-group col-4 mr-3">
<label for="name_{{view.id}}">Name</label>
<input type="text" class="form-control" formControlName="name" id="name_{{view.id}}">
</div>
<div class="form-group col-auto mr-3">
<label for="show_on_dashboard_{{view.id}}">Appears on</label>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_on_dashboard_{{view.id}}" formControlName="show_on_dashboard">
<label class="custom-control-label" for="show_on_dashboard_{{view.id}}">Show on dashboard</label>
</div>
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="show_in_sidebar_{{view.id}}" formControlName="show_in_sidebar">
<label class="custom-control-label" for="show_in_sidebar_{{view.id}}">Show in sidebar</label>
</div>
</div>
<div class="form-group col-auto">
<label for="name_{{view.id}}">Actions</label>
<button type="button" class="btn btn-sm btn-outline-danger form-control" (click)="deleteSavedView(view)">Delete</button>
</div>
</div>
<div *ngIf="savedViews.length == 0">No saved views defined.</div>
</div>
</ng-template>
</li>

View File

@@ -1,11 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { Title } from '@angular/platform-browser';
import { SavedViewConfig } from 'src/app/data/saved-view-config';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { GENERAL_SETTINGS } from 'src/app/data/storage-keys';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { SavedViewConfigService } from 'src/app/services/saved-view-config.service';
import { environment } from 'src/environments/environment';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { Toast, ToastService } from 'src/app/services/toast.service';
@Component({
selector: 'app-settings',
@@ -14,26 +13,53 @@ import { environment } from 'src/environments/environment';
})
export class SettingsComponent implements OnInit {
savedViewGroup = new FormGroup({})
settingsForm = new FormGroup({
'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT)
'documentListItemPerPage': new FormControl(+localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT),
'savedViews': this.savedViewGroup
})
constructor(
private savedViewConfigService: SavedViewConfigService,
public savedViewService: SavedViewService,
private documentListViewService: DocumentListViewService,
private titleService: Title
private toastService: ToastService
) { }
ngOnInit(): void {
this.titleService.setTitle(`Settings - ${environment.appTitle}`)
savedViews: PaperlessSavedView[]
ngOnInit() {
this.savedViewService.listAll().subscribe(r => {
this.savedViews = r.results
for (let view of this.savedViews) {
this.savedViewGroup.addControl(view.id.toString(), new FormGroup({
"id": new FormControl(view.id),
"name": new FormControl(view.name),
"show_on_dashboard": new FormControl(view.show_on_dashboard),
"show_in_sidebar": new FormControl(view.show_in_sidebar)
}))
}
})
}
deleteViewConfig(config: SavedViewConfig) {
this.savedViewConfigService.deleteConfig(config)
deleteSavedView(savedView: PaperlessSavedView) {
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewGroup.removeControl(savedView.id.toString())
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
this.toastService.showToast(Toast.make("Information", `Saved view "${savedView.name} deleted.`))
})
}
saveSettings() {
localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.documentListViewService.updatePageSize()
let x = []
for (let id in this.savedViewGroup.value) {
x.push(this.savedViewGroup.value[id])
}
this.savedViewService.patchMany(x).subscribe(s => {
this.toastService.showToast(Toast.make("Information", "Settings saved successfully."))
localStorage.setItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE, this.settingsForm.value.documentListItemPerPage)
this.documentListViewService.updatePageSize()
})
}
}

View File

@@ -1,9 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { Component } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TAG_COLOURS, PaperlessTag } from 'src/app/data/paperless-tag';
import { TagService } from 'src/app/services/rest/tag.service';
import { environment } from 'src/environments/environment';
import { GenericListComponent } from '../generic-list/generic-list.component';
import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.component';
@@ -12,18 +10,12 @@ import { TagEditDialogComponent } from './tag-edit-dialog/tag-edit-dialog.compon
templateUrl: './tag-list.component.html',
styleUrls: ['./tag-list.component.scss']
})
export class TagListComponent extends GenericListComponent<PaperlessTag> implements OnInit {
export class TagListComponent extends GenericListComponent<PaperlessTag> {
constructor(tagService: TagService, modalService: NgbModal, private titleService: Title) {
constructor(tagService: TagService, modalService: NgbModal) {
super(tagService, modalService, TagEditDialogComponent)
}
ngOnInit(): void {
super.ngOnInit()
this.titleService.setTitle(`Tags - ${environment.appTitle}`)
}
getColor(id) {
return TAG_COLOURS.find(c => c.id == id)
}

View File

@@ -1,9 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { SearchHit } from 'src/app/data/search-result';
import { SearchService } from 'src/app/services/rest/search.service';
import { environment } from 'src/environments/environment';
@Component({
selector: 'app-search',
@@ -28,7 +26,7 @@ export class SearchComponent implements OnInit {
errorMessage: string
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private titleService: Title) { }
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { }
ngOnInit(): void {
this.route.queryParamMap.subscribe(paramMap => {
@@ -36,7 +34,6 @@ export class SearchComponent implements OnInit {
this.searching = true
this.currentPage = 1
this.loadPage()
this.titleService.setTitle(`Search: ${this.query} - ${environment.appTitle}`)
})
}

View File

@@ -22,15 +22,15 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_CONTENT, name: "Content contains", filtervar: "content__icontains", datatype: "string", multi: false, default: ""},
{id: FILTER_ASN, name: "ASN is", filtervar: "archive_serial_number", datatype: "number", multi: false},
{id: FILTER_CORRESPONDENT, name: "Correspondent is", filtervar: "correspondent__id", datatype: "correspondent", multi: false},
{id: FILTER_DOCUMENT_TYPE, name: "Document type is", filtervar: "document_type__id", datatype: "document_type", multi: false},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true},
{id: FILTER_IS_IN_INBOX, name: "Is in Inbox", filtervar: "is_in_inbox", datatype: "boolean", multi: false, default: true},
{id: FILTER_HAS_TAG, name: "Has tag", filtervar: "tags__id__all", datatype: "tag", multi: true},
{id: FILTER_DOES_NOT_HAVE_TAG, name: "Does not have tag", filtervar: "tags__id__none", datatype: "tag", multi: true},
{id: FILTER_HAS_ANY_TAG, name: "Has any tag", filtervar: "is_tagged", datatype: "boolean", multi: false, default: true},
{id: FILTER_CREATED_BEFORE, name: "Created before", filtervar: "created__date__lt", datatype: "date", multi: false},
@@ -42,7 +42,7 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_ADDED_BEFORE, name: "Added before", filtervar: "added__date__lt", datatype: "date", multi: false},
{id: FILTER_ADDED_AFTER, name: "Added after", filtervar: "added__date__gt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_BEFORE, name: "Modified before", filtervar: "modified__date__lt", datatype: "date", multi: false},
{id: FILTER_MODIFIED_AFTER, name: "Modified after", filtervar: "modified__date__gt", datatype: "date", multi: false},
]
@@ -54,4 +54,4 @@ export interface FilterRuleType {
datatype: string //number, string, boolean, date
multi: boolean
default?: any
}
}

View File

@@ -1,10 +1,8 @@
import { FilterRuleType } from './filter-rule-type';
export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
if (filterRules) {
let newRules: FilterRule[] = []
for (let rule of filterRules) {
newRules.push({type: rule.type, value: rule.value})
newRules.push({rule_type: rule.rule_type, value: rule.value})
}
return newRules
} else {
@@ -13,6 +11,6 @@ export function cloneFilterRules(filterRules: FilterRule[]): FilterRule[] {
}
export interface FilterRule {
type: FilterRuleType
value: any
rule_type: number
value: string
}

View File

@@ -0,0 +1,18 @@
import { FilterRule } from './filter-rule';
import { ObjectWithId } from './object-with-id';
export interface PaperlessSavedView extends ObjectWithId {
name?: string
show_on_dashboard?: boolean
show_in_sidebar?: boolean
sort_field: string
sort_reverse: boolean
filter_rules: FilterRule[]
}

View File

@@ -1,19 +0,0 @@
import { FilterRule } from './filter-rule';
export interface SavedViewConfig {
id?: string
filterRules: FilterRule[]
sortField: string
sortDirection: string
title?: string
showInSideBar?: boolean
showInDashboard?: boolean
}

View File

@@ -0,0 +1,17 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'filter'
})
export class FilterPipe implements PipeTransform {
transform(items: any[], searchText: string): any[] {
if (!items) return [];
if (!searchText) return items;
return items.filter(item => {
return Object.keys(item).some(key => {
return String(item[key]).toLowerCase().includes(searchText.toLowerCase());
});
});
}
}

View File

@@ -2,14 +2,14 @@ import { Injectable } from '@angular/core';
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 { PaperlessSavedView } from '../data/paperless-saved-view';
import { DOCUMENT_LIST_SERVICE, GENERAL_SETTINGS } from '../data/storage-keys';
import { DocumentService } from './rest/document.service';
/**
* This service manages the document list which is displayed using the document list view.
*
*
* This service also serves saved views by transparently switching between the document list
* and saved views on request. See below.
*/
@@ -25,21 +25,21 @@ export class DocumentListViewService {
currentPage = 1
currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
collectionSize: number
/**
* This is the current config for the document list. The service will always remember the last settings used for the document list.
*/
private _documentListViewConfig: SavedViewConfig
private _documentListViewConfig: PaperlessSavedView
/**
* Optionally, this is the currently selected saved view, which might be null.
*/
private _savedViewConfig: SavedViewConfig
private _savedViewConfig: PaperlessSavedView
get savedView() {
get savedView(): PaperlessSavedView {
return this._savedViewConfig
}
set savedView(value) {
set savedView(value: PaperlessSavedView) {
if (value) {
//this is here so that we don't modify value, which might be the actual instance of the saved view.
this._savedViewConfig = Object.assign({}, value)
@@ -53,7 +53,7 @@ export class DocumentListViewService {
}
get savedViewTitle() {
return this.savedView?.title
return this.savedView?.name
}
get documentListView() {
@@ -75,11 +75,11 @@ export class DocumentListViewService {
return this.savedView || this.documentListView
}
load(config: SavedViewConfig) {
this.view.filterRules = cloneFilterRules(config.filterRules)
this.view.sortDirection = config.sortDirection
this.view.sortField = config.sortField
this.reload()
load(view: PaperlessSavedView) {
this.documentListView.filter_rules = cloneFilterRules(view.filter_rules)
this.documentListView.sort_reverse = view.sort_reverse
this.documentListView.sort_field = view.sort_field
this.saveDocumentListView()
}
clear() {
@@ -93,9 +93,9 @@ export class DocumentListViewService {
this.documentService.listFiltered(
this.currentPage,
this.currentPageSize,
this.view.sortField,
this.view.sortDirection,
this.view.filterRules).subscribe(
this.view.sort_field,
this.view.sort_reverse,
this.view.filter_rules).subscribe(
result => {
this.collectionSize = result.count
this.documents = result.results
@@ -116,34 +116,34 @@ export class DocumentListViewService {
set filterRules(filterRules: FilterRule[]) {
//we're going to clone the filterRules object, since we don't
//want changes in the filter editor to propagate into here right away.
this.view.filterRules = cloneFilterRules(filterRules)
this.view.filter_rules = cloneFilterRules(filterRules)
this.reload()
this.reduceSelectionToFilter()
this.saveDocumentListView()
}
get filterRules(): FilterRule[] {
return cloneFilterRules(this.view.filterRules)
return cloneFilterRules(this.view.filter_rules)
}
set sortField(field: string) {
this.view.sortField = field
this.view.sort_field = field
this.saveDocumentListView()
this.reload()
}
get sortField(): string {
return this.view.sortField
return this.view.sort_field
}
set sortDirection(direction: string) {
this.view.sortDirection = direction
set sortReverse(reverse: boolean) {
this.view.sort_reverse = reverse
this.saveDocumentListView()
this.reload()
}
get sortDirection(): string {
return this.view.sortDirection
get sortReverse(): boolean {
return this.view.sort_reverse
}
private saveDocumentListView() {
@@ -189,7 +189,6 @@ export class DocumentListViewService {
let newPageSize = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT
if (newPageSize != this.currentPageSize) {
this.currentPageSize = newPageSize
//this.reload()
}
}
@@ -236,7 +235,7 @@ export class DocumentListViewService {
}
}
constructor(private documentService: DocumentService) {
constructor(private documentService: DocumentService) {
let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG)
if (documentListViewConfigJson) {
try {
@@ -248,9 +247,9 @@ export class DocumentListViewService {
}
if (!this.documentListView) {
this.documentListView = {
filterRules: [],
sortDirection: 'des',
sortField: 'created'
filter_rules: [],
sort_reverse: true,
sort_field: 'created'
}
}
}

View File

@@ -1,5 +1,5 @@
import { HttpClient, HttpParams } from '@angular/common/http'
import { Observable, of, Subject } from 'rxjs'
import { Observable } from 'rxjs'
import { map, publishReplay, refCount } from 'rxjs/operators'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { Results } from 'src/app/data/results'
@@ -22,17 +22,15 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
return url
}
private getOrderingQueryParam(sortField: string, sortDirection: string) {
if (sortField && sortDirection) {
return (sortDirection == 'des' ? '-' : '') + sortField
} else if (sortField) {
return sortField
private getOrderingQueryParam(sortField: string, sortReverse: boolean) {
if (sortField) {
return (sortReverse ? '-' : '') + sortField
} else {
return null
}
}
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> {
list(page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, extraParams?): Observable<Results<T>> {
let httpParams = new HttpParams()
if (page) {
httpParams = httpParams.set('page', page.toString())
@@ -40,7 +38,7 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
if (pageSize) {
httpParams = httpParams.set('page_size', pageSize.toString())
}
let ordering = this.getOrderingQueryParam(sortField, sortDirection)
let ordering = this.getOrderingQueryParam(sortField, sortReverse)
if (ordering) {
httpParams = httpParams.set('ordering', ordering)
}
@@ -54,9 +52,9 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
private _listAll: Observable<Results<T>>
listAll(sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> {
listAll(sortField?: string, sortReverse?: boolean, extraParams?): Observable<Results<T>> {
if (!this._listAll) {
this._listAll = this.list(1, 100000, sortField, sortDirection, extraParams).pipe(
this._listAll = this.list(1, 100000, sortField, sortReverse, extraParams).pipe(
publishReplay(1),
refCount()
)
@@ -94,4 +92,10 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
this._listAll = null
return this.http.put<T>(this.getResourceUrl(o.id), o)
}
patch(o: T): Observable<T> {
this._listAll = null
return this.http.patch<T>(this.getResourceUrl(o.id), o)
}
}

View File

@@ -10,7 +10,7 @@ import { map } from 'rxjs/operators';
import { CorrespondentService } from './correspondent.service';
import { DocumentTypeService } from './document-type.service';
import { TagService } from './tag.service';
import { FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type';
export const DOCUMENT_SORT_FIELDS = [
{ field: "correspondent__name", name: "Correspondent" },
@@ -22,10 +22,6 @@ export const DOCUMENT_SORT_FIELDS = [
{ field: 'modified', name: 'Modified' }
]
export const SORT_DIRECTION_ASCENDING = "asc"
export const SORT_DIRECTION_DESCENDING = "des"
@Injectable({
providedIn: 'root'
})
@@ -39,10 +35,11 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
if (filterRules) {
let params = {}
for (let rule of filterRules) {
if (rule.type.multi) {
params[rule.type.filtervar] = params[rule.type.filtervar] ? params[rule.type.filtervar] + "," + rule.value : rule.value
let ruleType = FILTER_RULE_TYPES.find(t => t.id == rule.rule_type)
if (ruleType.multi) {
params[ruleType.filtervar] = params[ruleType.filtervar] ? params[ruleType.filtervar] + "," + rule.value : rule.value
} else {
params[rule.type.filtervar] = rule.value
params[ruleType.filtervar] = rule.value
}
}
return params
@@ -64,8 +61,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
return doc
}
listFiltered(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[], extraParams = {}): Observable<Results<PaperlessDocument>> {
return this.list(page, pageSize, sortField, sortDirection, Object.assign(extraParams, this.filterRulesToQueryParams(filterRules))).pipe(
listFiltered(page?: number, pageSize?: number, sortField?: string, sortReverse?: boolean, filterRules?: FilterRule[], extraParams = {}): Observable<Results<PaperlessDocument>> {
return this.list(page, pageSize, sortField, sortReverse, Object.assign(extraParams, this.filterRulesToQueryParams(filterRules))).pipe(
map(results => {
results.results.forEach(doc => this.addObservablesToDocument(doc))
return results

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { SavedViewService } from './saved-view.service';
describe('SavedViewService', () => {
let service: SavedViewService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SavedViewService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,59 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { AbstractPaperlessService } from './abstract-paperless-service';
@Injectable({
providedIn: 'root'
})
export class SavedViewService extends AbstractPaperlessService<PaperlessSavedView> {
constructor(http: HttpClient) {
super(http, 'saved_views')
this.reload()
}
private reload() {
this.listAll().subscribe(r => this.savedViews = r.results)
}
private savedViews: PaperlessSavedView[] = []
get allViews() {
return this.savedViews
}
get sidebarViews() {
return this.savedViews.filter(v => v.show_in_sidebar)
}
get dashboardViews() {
return this.savedViews.filter(v => v.show_on_dashboard)
}
create(o: PaperlessSavedView) {
return super.create(o).pipe(
tap(() => this.reload())
)
}
update(o: PaperlessSavedView) {
return super.update(o).pipe(
tap(() => this.reload())
)
}
patchMany(objects: PaperlessSavedView[]): Observable<PaperlessSavedView[]> {
return combineLatest(objects.map(o => super.patch(o))).pipe(
tap(() => this.reload())
)
}
delete(o: PaperlessSavedView) {
return super.delete(o).pipe(
tap(() => this.reload())
)
}
}

View File

@@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { SavedViewConfigService } from './saved-view-config.service';
describe('SavedViewConfigService', () => {
let service: SavedViewConfigService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(SavedViewConfigService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -1,66 +0,0 @@
import { Injectable } from '@angular/core';
import { v4 as uuidv4 } from 'uuid';
import { SavedViewConfig } from '../data/saved-view-config';
@Injectable({
providedIn: 'root'
})
export class SavedViewConfigService {
constructor() {
let savedConfigs = localStorage.getItem('saved-view-config-service:savedConfigs')
if (savedConfigs) {
try {
this.configs = JSON.parse(savedConfigs)
} catch (e) {
this.configs = []
}
}
}
private configs: SavedViewConfig[] = []
getConfigs(): SavedViewConfig[] {
return this.configs
}
getDashboardConfigs(): SavedViewConfig[] {
return this.configs.filter(sf => sf.showInDashboard)
}
getSideBarConfigs(): SavedViewConfig[] {
return this.configs.filter(sf => sf.showInSideBar)
}
getConfig(id: string): SavedViewConfig {
return this.configs.find(sf => sf.id == id)
}
newConfig(config: SavedViewConfig) {
config.id = uuidv4()
this.configs.push(config)
this.save()
}
updateConfig(config: SavedViewConfig) {
let savedConfig = this.configs.find(c => c.id == config.id)
if (savedConfig) {
Object.assign(savedConfig, config)
this.save()
}
}
private save() {
localStorage.setItem('saved-view-config-service:savedConfigs', JSON.stringify(this.configs))
}
deleteConfig(config: SavedViewConfig) {
let index = this.configs.findIndex(vc => vc.id == config.id)
if (index != -1) {
this.configs.splice(index, 1)
this.save()
}
}
}