Merge branch 'dev' into feature-bulk-edit

This commit is contained in:
jonaswinkler
2020-12-21 23:20:55 +01:00
54 changed files with 965 additions and 930 deletions

View File

@@ -54,6 +54,7 @@ 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 { NgSelectModule } from '@ng-select/ng-select';
import { SelectDialogComponent } from './components/common/select-dialog/select-dialog.component';
@NgModule({
@@ -112,7 +113,8 @@ import { SelectDialogComponent } from './components/common/select-dialog/select-
ReactiveFormsModule,
NgxFileDropModule,
InfiniteScrollModule,
PdfViewerModule
PdfViewerModule,
NgSelectModule
],
providers: [
DatePipe,

View File

@@ -1,11 +1,15 @@
<div class="form-group">
<div class="form-group paperless-input-select">
<label [for]="inputId">{{title}}</label>
<div [class.input-group]="showPlusButton()">
<select class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()"
[disabled]="disabled" [style.color]="textColor" [style.background]="backgroundColor">
<option *ngIf="allowNull" [ngValue]="null" class="form-control">---</option>
<option *ngFor="let i of items" [ngValue]="i.id" class="form-control">{{i.name}}</option>
</select>
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
[style.color]="textColor"
[style.background]="backgroundColor"
(change)="onChange(value)"
(blur)="onTouched()">
<ng-option *ngFor="let i of items" [value]="i.id">{{i.name}}</ng-option>
</ng-select>
<div *ngIf="showPlusButton()" class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createNew.emit()">
<svg class="buttonicon" fill="currentColor">
@@ -15,4 +19,4 @@
</div>
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</div>
</div>

View File

@@ -0,0 +1 @@
// styles for ng-select child are in styles.scss

View File

@@ -1,30 +1,41 @@
<div class="form-group">
<label for="exampleFormControlTextarea1">Tags</label>
<div class="form-group paperless-input-select paperless-input-tags">
<label for="tags">Tags</label>
<div class="input-group">
<div class="form-control tags-form-control" id="tags">
<app-tag class="mr-2" *ngFor="let id of displayValue" [tag]="getTag(id)" (click)="removeTag(id)"></app-tag>
</div>
<div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"
[multiple]="true"
[closeOnSelect]="false"
[disabled]="disabled"
(change)="ngSelectChange()">
<div class="input-group-append" ngbDropdown placement="top-right">
<button class="btn btn-outline-secondary" type="button" ngbDropdownToggle></button>
<div ngbDropdownMenu class="scrollable-menu shadow">
<button type="button" *ngFor="let tag of tags" ngbDropdownItem (click)="addTag(tag.id)">
<app-tag [tag]="tag"></app-tag>
</button>
</div>
</div>
<ng-template ng-label-tmp let-item="item">
<span class="tag-wrap tag-wrap-delete" (click)="removeTag(item.id)">
<svg width="1.2em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#x"/>
</svg>
<app-tag style="background-color: none;" [tag]="getTag(item.id)"></app-tag>
</span>
</ng-template>
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div class="tag-wrap">
<div class="selected-icon d-inline-block mr-1">
<svg *ngIf="displayValue.includes(item.id)" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<use xlink:href="assets/bootstrap-icons.svg#check"/>
</svg>
</div>
<app-tag class="mr-2" [tag]="getTag(item.id)"></app-tag>
</div>
</ng-template>
</ng-select>
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" (click)="createTag()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus" />
</svg>
</button>
</div>
</div>
<small class="form-text text-muted" *ngIf="hint">{{hint}}</small>
</div>
</div>

View File

@@ -1,10 +1,12 @@
.tags-form-control {
height: auto;
.selected-icon {
min-width: 1em;
min-height: 1em;
}
.tag-wrap {
font-size: 1rem;
}
.scrollable-menu {
height: auto;
max-height: 300px;
overflow-x: hidden;
}
.tag-wrap-delete {
cursor: pointer;
}

View File

@@ -21,7 +21,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
onChange = (newValue: number[]) => {};
onTouched = () => {};
writeValue(newValue: number[]): void {
@@ -66,29 +66,28 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
removeTag(id) {
let index = this.displayValue.indexOf(id)
if (index > -1) {
this.displayValue.splice(index, 1)
let oldValue = this.displayValue
oldValue.splice(index, 1)
this.displayValue = [...oldValue]
this.onChange(this.displayValue)
}
}
addTag(id) {
let index = this.displayValue.indexOf(id)
if (index == -1) {
this.displayValue.push(id)
this.onChange(this.displayValue)
}
}
createTag() {
var modal = this.modalService.open(TagEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
modal.componentInstance.success.subscribe(newTag => {
this.tagService.listAll().subscribe(tags => {
this.tags = tags.results
this.addTag(newTag.id)
this.displayValue = [...this.displayValue, newTag.id]
this.onChange(this.displayValue)
})
})
}
ngSelectChange() {
this.value = this.displayValue
this.onChange(this.displayValue)
}
}

View File

@@ -1,4 +1,4 @@
<app-page-header title="Dashboard" subTitle="Welcome to paperless-ng!">
<app-page-header title="Dashboard" [subTitle]="subtitle">
<img src="assets/logo.svg" height="80" class="m-2 d-none d-md-block">
</app-page-header>

View File

@@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Meta } from '@angular/platform-browser';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@@ -11,8 +12,29 @@ import { SavedViewService } from 'src/app/services/rest/saved-view.service';
export class DashboardComponent implements OnInit {
constructor(
private savedViewService: SavedViewService) { }
private savedViewService: SavedViewService,
private meta: Meta
) { }
get displayName() {
let tagFullName = this.meta.getTag('name=full_name')
let tagUsername = this.meta.getTag('name=username')
if (tagFullName && tagFullName.content) {
return tagFullName.content
} else if (tagUsername && tagUsername.content) {
return tagUsername.content
} else {
return null
}
}
get subtitle() {
if (this.displayName) {
return `Hello ${this.displayName}, welcome to Paperless-ng!`
} else {
return `Welcome to Paperless-ng!`
}
}
savedViews: PaperlessSavedView[] = []

View File

@@ -1,4 +1,14 @@
<app-page-header [(title)]="title">
<div class="input-group input-group-sm mr-5" *ngIf="getContentType() == 'application/pdf'">
<div class="input-group-prepend">
<div class="input-group-text">Page </div>
</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
<div class="input-group-append">
<div class="input-group-text">of {{previewNumPages}}</div>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-danger mr-2" (click)="delete()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
@@ -24,6 +34,12 @@
</div>
<button type="button" class="btn btn-sm btn-outline-primary mr-2" (click)="moreLike()">
<svg class="buttonicon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#three-dots" />
</svg>
<span class="d-none d-lg-inline"> More like this</span>
</button>
<button type="button" class="btn btn-sm btn-outline-primary" (click)="close()">
<svg class="buttonicon" fill="currentColor">
@@ -52,9 +68,9 @@
</div>
<app-input-date-time titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" title="Correspondent" formControlName="correspondent"
allowNull="true" (createNew)="createCorrespondent()"></app-input-select>
(createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" title="Document type" formControlName="document_type"
allowNull="true" (createNew)="createDocumentType()"></app-input-select>
(createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" title="Tags"></app-input-tags>
</ng-template>
@@ -128,7 +144,7 @@
<div class="col-md-6 col-xl-8 mb-3">
<div class="pdf-viewer-container" *ngIf="getContentType() == 'application/pdf'">
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true"></pdf-viewer>
<pdf-viewer [src]="previewUrl" [original-size]="false" [show-borders]="true" [show-all]="true" [(page)]="previewCurrentPage" (after-load-complete)="pdfPreviewLoaded($event)"></pdf-viewer>
</div>
</div>
</div>

View File

@@ -15,6 +15,7 @@ import { DocumentService } from 'src/app/services/rest/document.service';
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component';
import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/correspondent-edit-dialog/correspondent-edit-dialog.component';
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
@Component({
selector: 'app-document-detail',
@@ -47,8 +48,11 @@ export class DocumentDetailComponent implements OnInit {
tags: new FormControl([])
})
previewCurrentPage: number = 1
previewNumPages: number = 1
constructor(
private documentsService: DocumentService,
private documentsService: DocumentService,
private route: ActivatedRoute,
private correspondentService: CorrespondentService,
private documentTypeService: DocumentTypeService,
@@ -126,7 +130,7 @@ export class DocumentDetailComponent implements OnInit {
}, error => {this.router.navigate(['404'])})
}
save() {
save() {
this.documentsService.update(this.document).subscribe(result => {
this.close()
})
@@ -161,14 +165,23 @@ export class DocumentDetailComponent implements OnInit {
modal.componentInstance.btnCaption = "Delete document"
modal.componentInstance.confirmClicked.subscribe(() => {
this.documentsService.delete(this.document).subscribe(() => {
modal.close()
modal.close()
this.close()
})
})
}
moreLike() {
this.router.navigate(["search"], {queryParams: {more_like:this.document.id}})
}
hasNext() {
return this.documentListViewService.hasNext(this.documentId)
}
pdfPreviewLoaded(pdf: PDFDocumentProxy) {
this.previewNumPages = pdf.numPages
}
}

View File

@@ -23,8 +23,14 @@
</p>
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<div class="btn-group">
<a routerLink="/search" [queryParams]="{'more_like': document.id}" class="btn btn-sm btn-outline-secondary" *ngIf="moreLikeThis">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-three-dots" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z"/>
</svg>
More like this
</a>
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary">
<svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-pencil" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168l10-10zM11.207 2.5L13.5 4.793 14.793 3.5 12.5 1.207 11.207 2.5zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293l6.5-6.5zm-9.761 5.175l-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325z"/>
@@ -45,10 +51,16 @@
</svg>
Download
</a>
</div>
<small class="text-muted ml-auto">Score:</small>
<ngb-progressbar *ngIf="searchScore" [type]="searchScoreClass" [value]="searchScore" class="search-score-bar mx-2" [max]="1"></ngb-progressbar>
<small class="text-muted">Created: {{document.created | date}}</small>
</div>
</div>
</div>
</div>

View File

@@ -9,4 +9,10 @@
height: 100%;
position: absolute;
}
.search-score-bar {
width: 100px;
height: 5px;
margin-top: 2px;
}

View File

@@ -12,6 +12,9 @@ export class DocumentCardLargeComponent implements OnInit {
constructor(private documentService: DocumentService, private sanitizer: DomSanitizer) { }
@Input()
moreLikeThis: boolean = false
@Input()
document: PaperlessDocument
@@ -24,6 +27,19 @@ export class DocumentCardLargeComponent implements OnInit {
@Output()
clickCorrespondent = new EventEmitter<number>()
@Input()
searchScore: number
get searchScoreClass() {
if (this.searchScore > 0.7) {
return "success"
} else if (this.searchScore > 0.3) {
return "warning"
} else {
return "danger"
}
}
ngOnInit(): void {
}

View File

@@ -4,38 +4,39 @@
</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 *ngFor="let qf of quickFilters" class="list-group-item small list-goup list-group-item-action d-flex p-2 pl-3" role="menuitem" (click)="setDateQuickFilter(qf.id)">
{{qf.name}}
</button>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div>Before</div>
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div>After</div>
<a *ngIf="dateAfter" class="btn btn-link p-0 m-0" (click)="clearAfter()">
<svg width="0.8em" height="0.8em" 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>
<small>Clear</small>
</a>
</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>
<input type="date" class="form-control" id="date_after" [(ngModel)]="dateAfter" (change)="onChangeDebounce()">
</div>
</div>
<div class="list-group-item d-flex flex-column align-items-start" role="menuitem">
<div>After</div>
<div class="mb-2 d-flex flex-row w-100 justify-content-between small">
<div>Before</div>
<a *ngIf="dateBefore" class="btn btn-link p-0 m-0" (click)="clearBefore()">
<svg width="0.8em" height="0.8em" 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>
<small>Clear</small>
</a>
</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>
<input type="date" class="form-control" id="date_before" [(ngModel)]="dateBefore" (change)="onChangeDebounce()">
</div>
</div>
</div>

View File

@@ -1,24 +1,37 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild, SimpleChange } from '@angular/core';
import { NgbDate, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap';
import { formatDate } from '@angular/common';
import { Component, EventEmitter, Input, Output, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
export interface DateSelection {
before?: NgbDateStruct
after?: NgbDateStruct
before?: string
after?: string
}
const FILTER_LAST_7_DAYS = 0
const FILTER_LAST_MONTH = 1
const FILTER_LAST_3_MONTHS = 2
const FILTER_LAST_YEAR = 3
@Component({
selector: 'app-filter-dropdown-date',
templateUrl: './filter-dropdown-date.component.html',
styleUrls: ['./filter-dropdown-date.component.scss']
})
export class FilterDropdownDateComponent {
export class FilterDropdownDateComponent implements OnInit, OnDestroy {
quickFilters = [
{id: FILTER_LAST_7_DAYS, name: "Last 7 days"},
{id: FILTER_LAST_MONTH, name: "Last month"},
{id: FILTER_LAST_3_MONTHS, name: "Last 3 months"},
{id: FILTER_LAST_YEAR, name: "Last year"}
]
@Input()
dateBefore: NgbDateStruct
dateBefore: string
@Input()
dateAfter: NgbDateStruct
dateAfter: string
@Input()
title: string
@@ -26,87 +39,65 @@ export class FilterDropdownDateComponent {
@Output()
datesSet = new EventEmitter<DateSelection>()
@ViewChild('dpAfter') dpAfter: NgbDatepicker
@ViewChild('dpBefore') dpBefore: NgbDatepicker
private datesSetDebounce$ = new Subject()
_dateBefore: NgbDateStruct
_dateAfter: NgbDateStruct
get _maxDate(): NgbDate {
let date = new Date()
return NgbDate.from({year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()})
private sub: Subscription
ngOnInit() {
this.sub = this.datesSetDebounce$.pipe(
debounceTime(400)
).subscribe(() => {
this.onChange()
})
}
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']
ngOnDestroy() {
if (this.sub) {
this.sub.unsubscribe()
}
}
if (this.dpBefore && this.dpAfter) {
let dpAfterElRef: ElementRef = this.dpAfter['_elRef']
let dpBeforeElRef: ElementRef = this.dpBefore['_elRef']
setDateQuickFilter(qf: number) {
this.dateBefore = null
let date = new Date()
switch (qf) {
case FILTER_LAST_7_DAYS:
date.setDate(date.getDate() - 7)
break;
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
case FILTER_LAST_MONTH:
date.setMonth(date.getMonth() - 1)
break;
case FILTER_LAST_3_MONTHS:
date.setMonth(date.getMonth() - 3)
break
case FILTER_LAST_YEAR:
date.setFullYear(date.getFullYear() - 1)
break
}
}
this.dateAfter = formatDate(date, 'yyyy-MM-dd', "en-us", "UTC")
this.onChange()
}
setDateQuickFilter(range: any) {
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._dateBefore = null
this.datesSet.emit({after: newDate, before: null})
onChange() {
this.datesSet.emit({after: this.dateAfter, before: this.dateBefore})
}
onBeforeSelected(date: NgbDateStruct) {
this._dateBefore = date
this.datesSet.emit({after: this._dateAfter, before: date})
onChangeDebounce() {
this.datesSetDebounce$.next({after: this.dateAfter, before: this.dateBefore})
}
onAfterSelected(date: NgbDateStruct) {
this._dateAfter = date
this.datesSet.emit({after: date, before: this._dateBefore})
clearBefore() {
this.dateBefore = null;
this.onChange()
}
clear() {
this._dateBefore = null
this._dateAfter = null
this.datesSet.emit({after: null, before: null})
clearAfter() {
this.dateAfter = null;
this.onChange()
}
}

View File

@@ -179,54 +179,53 @@ export class FilterEditorComponent implements OnInit, OnDestroy {
this.applyFilters()
}
get dateCreatedBefore(): NgbDateStruct {
get dateCreatedBefore(): string {
let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_BEFORE)
return createdBeforeRule ? this.dateParser.parse(createdBeforeRule.value) : null
return createdBeforeRule ? createdBeforeRule.value : null
}
get dateCreatedAfter(): NgbDateStruct {
get dateCreatedAfter(): string {
let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_CREATED_AFTER)
return createdAfterRule ? this.dateParser.parse(createdAfterRule.value) : null
return createdAfterRule ? createdAfterRule.value : null
}
get dateAddedBefore(): NgbDateStruct {
get dateAddedBefore(): string {
let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_BEFORE)
return addedBeforeRule ? this.dateParser.parse(addedBeforeRule.value) : null
return addedBeforeRule ? addedBeforeRule.value : null
}
get dateAddedAfter(): NgbDateStruct {
get dateAddedAfter(): string {
let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.rule_type == FILTER_ADDED_AFTER)
return addedAfterRule ? this.dateParser.parse(addedAfterRule.value) : null
return addedAfterRule ? addedAfterRule.value : null
}
setDateCreatedBefore(date?: NgbDateStruct) {
setDateCreatedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_BEFORE)
else this.clearDateFilter(FILTER_CREATED_BEFORE)
}
setDateCreatedAfter(date?: NgbDateStruct) {
setDateCreatedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_CREATED_AFTER)
else this.clearDateFilter(FILTER_CREATED_AFTER)
}
setDateAddedBefore(date?: NgbDateStruct) {
setDateAddedBefore(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_BEFORE)
else this.clearDateFilter(FILTER_ADDED_BEFORE)
}
setDateAddedAfter(date?: NgbDateStruct) {
setDateAddedAfter(date?: string) {
if (date) this.setDateFilter(date, FILTER_ADDED_AFTER)
else this.clearDateFilter(FILTER_ADDED_AFTER)
}
setDateFilter(date: NgbDateStruct, dateRuleTypeID: number) {
setDateFilter(date: string, dateRuleTypeID: number) {
let existingRule = this.filterRules.find(rule => rule.rule_type == dateRuleTypeID)
let newValue = this.dateParser.format(date)
if (existingRule) {
existingRule.value = newValue
existingRule.value = date
} else {
this.filterRules.push({rule_type: dateRuleTypeID, value: newValue})
this.filterRules.push({rule_type: dateRuleTypeID, value: date})
}
}

View File

@@ -8,10 +8,9 @@
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-text title="Match" formControlName="match"></app-input-text>
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>

View File

@@ -8,9 +8,9 @@
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-text title="Match" formControlName="match"></app-input-text>
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">

View File

@@ -7,11 +7,21 @@
</div>
<div class="modal-body">
<app-input-text title="Name" formControlName="name"></app-input-text>
<app-input-select title="Colour" [items]="getColours()" formControlName="colour" [textColor]="getColor(objectForm.value.colour).textColor" [backgroundColor]="getColor(objectForm.value.colour).value"></app-input-select>
<div class="form-group paperless-input-select">
<label for="colour">Colour</label>
<ng-select name="colour" formControlName="colour" [items]="getColours()" bindValue="id" bindLabel="name" [clearable]="false">
<ng-template ng-option-tmp ng-label-tmp let-item="item">
<span class="badge" [style.background]="item.value" [style.color]="item.textColor">{{item.name}}</span>
</ng-template>
</ng-select>
</div>
<app-input-check title="Inbox tag" formControlName="is_inbox_tag" hint="Inbox tags are automatically assigned to all consumed documents."></app-input-check>
<app-input-text title="Match" formControlName="match"></app-input-text>
<app-input-select title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></app-input-select>
<app-input-check title="Case insensitive" formControlName="is_insensitive"></app-input-check>
<app-input-text title="Match" formControlName="match" hint="Auto matching does not require you to fill in this field."></app-input-text>
<app-input-check title="Case insensitive" formControlName="is_insensitive" hint="Auto matching ignores this option."></app-input-check>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancel()">Cancel</button>

View File

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

View File

@@ -1,4 +1,4 @@
.match {
color: black;
background-color: orange;
background-color: rgb(255, 211, 66);
}

View File

@@ -3,7 +3,12 @@
<div *ngIf="errorMessage" class="alert alert-danger">Invalid search query: {{errorMessage}}</div>
<p>
<p *ngIf="more_like">
Showing documents similar to
<a routerLink="/documents/{{more_like}}">{{more_like_doc?.original_file_name}}</a>
</p>
<p *ngIf="query">
Search string: <i>{{query}}</i>
<ng-container *ngIf="correctedQuery">
- Did you mean "<a [routerLink]="" (click)="searchCorrectedQuery()">{{correctedQuery}}</a>"?
@@ -15,7 +20,9 @@
<p>{{resultCount}} result(s)</p>
<app-document-card-large *ngFor="let result of results"
[document]="result.document"
[details]="result.highlights">
[details]="result.highlights"
[searchScore]="result.score / maxScore"
[moreLikeThis]="true">
</app-document-card-large>
</div>

View File

@@ -1,6 +1,9 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type';
import { SearchHit } from 'src/app/data/search-result';
import { DocumentService } from 'src/app/services/rest/document.service';
import { SearchService } from 'src/app/services/rest/search.service';
@Component({
@@ -14,6 +17,10 @@ export class SearchComponent implements OnInit {
query: string = ""
more_like: number
more_like_doc: PaperlessDocument
searching = false
currentPage = 1
@@ -26,11 +33,24 @@ export class SearchComponent implements OnInit {
errorMessage: string
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router) { }
get maxScore() {
return this.results?.length > 0 ? this.results[0].score : 100
}
constructor(private searchService: SearchService, private route: ActivatedRoute, private router: Router, private documentService: DocumentService) { }
ngOnInit(): void {
this.route.queryParamMap.subscribe(paramMap => {
window.scrollTo(0, 0)
this.query = paramMap.get('query')
this.more_like = paramMap.has('more_like') ? +paramMap.get('more_like') : null
if (this.more_like) {
this.documentService.get(this.more_like).subscribe(r => {
this.more_like_doc = r
})
} else {
this.more_like_doc = null
}
this.searching = true
this.currentPage = 1
this.loadPage()
@@ -39,13 +59,14 @@ export class SearchComponent implements OnInit {
}
searchCorrectedQuery() {
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery}})
this.router.navigate(["search"], {queryParams: {query: this.correctedQuery, more_like: this.more_like}})
}
loadPage(append: boolean = false) {
this.errorMessage = null
this.correctedQuery = null
this.searchService.search(this.query, this.currentPage).subscribe(result => {
this.searchService.search(this.query, this.currentPage, this.more_like).subscribe(result => {
if (append) {
this.results.push(...result.results)
} else {

View File

@@ -6,14 +6,14 @@ export const TAG_COLOURS = [
{id: 1, value: "#a6cee3", name: "Light Blue", textColor: "#000000"},
{id: 2, value: "#1f78b4", name: "Blue", textColor: "#ffffff"},
{id: 3, value: "#b2df8a", name: "Light Green", textColor: "#000000"},
{id: 4, value: "#33a02c", name: "Green", textColor: "#000000"},
{id: 4, value: "#33a02c", name: "Green", textColor: "#ffffff"},
{id: 5, value: "#fb9a99", name: "Light Red", textColor: "#000000"},
{id: 6, value: "#e31a1c", name: "Red ", textColor: "#ffffff"},
{id: 7, value: "#fdbf6f", name: "Light Orange", textColor: "#000000"},
{id: 8, value: "#ff7f00", name: "Orange", textColor: "#000000"},
{id: 9, value: "#cab2d6", name: "Light Violet", textColor: "#000000"},
{id: 10, value: "#6a3d9a", name: "Violet", textColor: "#ffffff"},
{id: 11, value: "#b15928", name: "Brown", textColor: "#000000"},
{id: 11, value: "#b15928", name: "Brown", textColor: "#ffffff"},
{id: 12, value: "#000000", name: "Black", textColor: "#ffffff"},
{id: 13, value: "#cccccc", name: "Light Grey", textColor: "#000000"}
]

View File

@@ -15,11 +15,17 @@ export class SearchService {
constructor(private http: HttpClient, private documentService: DocumentService) { }
search(query: string, page?: number): Observable<SearchResult> {
let httpParams = new HttpParams().set('query', query)
search(query: string, page?: number, more_like?: number): Observable<SearchResult> {
let httpParams = new HttpParams()
if (query) {
httpParams = httpParams.set('query', query)
}
if (page) {
httpParams = httpParams.set('page', page.toString())
}
if (more_like) {
httpParams = httpParams.set('more_like', more_like.toString())
}
return this.http.get<SearchResult>(`${environment.apiBaseUrl}search/`, {params: httpParams}).pipe(
map(result => {
result.results.forEach(hit => this.documentService.addObservablesToDocument(hit.document))