Merge remote-tracking branch 'upstream/dev' into fix/issue-267

This commit is contained in:
Michael Shamoon
2021-01-06 07:55:19 -08:00
50 changed files with 3302 additions and 354 deletions

View File

@@ -140,6 +140,13 @@
</svg>&nbsp;<ng-container i18n>Logs</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" routerLink="settings" routerLinkActive="active" (click)="closeMenu()">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#gear"/>
</svg>&nbsp;<ng-container i18n>Settings</ng-container>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="admin/">
<svg class="sidebaricon" fill="currentColor">

View File

@@ -9,7 +9,7 @@
<p *ngIf="message">{{message}}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled">Cancel</button>
<button type="button" class="btn btn-outline-dark" (click)="cancelClicked()" [disabled]="!buttonsEnabled" i18n>Cancel</button>
<button type="button" class="btn" [class]="btnClass" (click)="confirmClicked.emit()" [disabled]="!confirmButtonEnabled || !buttonsEnabled">
{{btnCaption}}
<span *ngIf="!confirmButtonEnabled"> ({{seconds}})</span>

View File

@@ -1,10 +1,13 @@
import { Directive, Input, OnInit } from '@angular/core';
import { Directive, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { ControlValueAccessor } from '@angular/forms';
import { v4 as uuidv4 } from 'uuid';
@Directive()
export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
@ViewChild("inputField")
inputField: ElementRef
constructor() { }
onChange = (newValue: T) => {};
@@ -24,6 +27,12 @@ export class AbstractInputComponent<T> implements OnInit, ControlValueAccessor {
this.disabled = isDisabled;
}
focus() {
if (this.inputField && this.inputField.nativeElement) {
this.inputField.nativeElement.focus()
}
}
@Input()
title: string

View File

@@ -1,8 +1,14 @@
<div class="form-group">
<label [for]="inputId">{{title}}</label>
<input type="number" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<div class="input-group" [class.is-invalid]="error">
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error">
<div class="input-group-append">
<button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button>
</div>
</div>
<div class="invalid-feedback">
{{error}}
</div>
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
</div>

View File

@@ -1,5 +1,7 @@
import { Component, forwardRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type';
import { DocumentService } from 'src/app/services/rest/document.service';
import { AbstractInputComponent } from '../abstract-input';
@Component({
@@ -14,8 +16,24 @@ import { AbstractInputComponent } from '../abstract-input';
})
export class NumberComponent extends AbstractInputComponent<number> {
constructor() {
constructor(private documentService: DocumentService) {
super()
}
nextAsn() {
if (this.value) {
return
}
this.documentService.listFiltered(1, 1, "archive_serial_number", true, [{rule_type: FILTER_ASN_ISNULL, value: "false"}]).subscribe(
results => {
if (results.count > 0) {
this.value = results.results[0].archive_serial_number + 1
} else {
this.value + 1
}
this.onChange(this.value)
}
)
}
}

View File

@@ -1,5 +1,5 @@
<div class="form-group paperless-input-select paperless-input-tags">
<label for="tags">Tags</label>
<label for="tags" i18n>Tags</label>
<div class="input-group flex-nowrap">
<ng-select name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="displayValue"

View File

@@ -1,6 +1,6 @@
<div class="form-group">
<label [for]="inputId">{{title}}</label>
<input type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<input #inputField type="text" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
<small *ngIf="hint" class="form-text text-muted">{{hint}}</small>
<div class="invalid-feedback">
{{error}}

View File

@@ -56,14 +56,14 @@
<a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent>
<app-input-text i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
<app-input-text #inputTitle i18n-title title="Title" formControlName="title" [error]="error?.title"></app-input-text>
<app-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" formControlName='archive_serial_number'></app-input-number>
<app-input-date-time i18n-titleDate titleDate="Date created" formControlName="created"></app-input-date-time>
<app-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true"
(createNew)="createCorrespondent()"></app-input-select>
<app-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true"
(createNew)="createDocumentType()"></app-input-select>
<app-input-tags formControlName="tags" i18n-title title="Tags"></app-input-tags>
<app-input-tags formControlName="tags"></app-input-tags>
</ng-template>
</li>

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
@@ -17,6 +17,7 @@ import { CorrespondentEditDialogComponent } from '../manage/correspondent-list/c
import { DocumentTypeEditDialogComponent } from '../manage/document-type-list/document-type-edit-dialog/document-type-edit-dialog.component';
import { PDFDocumentProxy } from 'ng2-pdf-viewer';
import { ToastService } from 'src/app/services/toast.service';
import { TextComponent } from '../common/input/text/text.component';
@Component({
selector: 'app-document-detail',
@@ -25,6 +26,9 @@ import { ToastService } from 'src/app/services/toast.service';
})
export class DocumentDetailComponent implements OnInit {
@ViewChild("inputTitle")
titleInput: TextComponent
expandOriginalMetadata = false
expandArchivedMetadata = false
@@ -157,6 +161,7 @@ export class DocumentDetailComponent implements OnInit {
if (nextDocId) {
this.openDocumentService.closeDocument(this.document)
this.router.navigate(['documents', nextDocId])
this.titleInput.focus()
}
}, error => {
this.networkActive = false

View File

@@ -83,8 +83,10 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<p i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</p>
<p i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</p>
<p>
<span i18n *ngIf="list.selected.size > 0">{list.collectionSize, plural, =1 {Selected {{list.selected.size}} of one document} other {Selected {{list.selected.size}} of {{list.collectionSize || 0}} documents}}</span>
<span i18n *ngIf="list.selected.size == 0">{list.collectionSize, plural, =1 {One document} other {{{list.collectionSize || 0}} documents}}</span>&nbsp;<span i18n *ngIf="isFiltered">(filtered)</span>
</p>
<ngb-pagination [pageSize]="list.currentPageSize" [collectionSize]="list.collectionSize" [(page)]="list.currentPage" [maxSize]="5"
[rotate]="true" (pageChange)="list.reload()" aria-label="Default pagination"></ngb-pagination>
</div>
@@ -97,12 +99,42 @@
<table class="table table-sm border shadow-sm" *ngIf="displayMode == 'details'">
<thead>
<th></th>
<th class="d-none d-lg-table-cell" i18n>ASN</th>
<th class="d-none d-md-table-cell" i18n>Correspondent</th>
<th i18n>Title</th>
<th class="d-none d-xl-table-cell" i18n>Document type</th>
<th i18n>Created</th>
<th class="d-none d-xl-table-cell" i18n>Added</th>
<th class="d-none d-lg-table-cell"
sortable="archive_serial_number"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>ASN</th>
<th class="d-none d-md-table-cell"
sortable="correspondent__name"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Correspondent</th>
<th
sortable="title"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Title</th>
<th class="d-none d-xl-table-cell"
sortable="document_type__name"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Document type</th>
<th
sortable="created"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Created</th>
<th class="d-none d-xl-table-cell"
sortable="added"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
i18n>Added</th>
</thead>
<tbody>
<tr *ngFor="let d of list.documents; trackBy: trackByDocumentId" [ngClass]="list.isSelected(d) ? 'table-row-selected' : ''">

View File

@@ -1,8 +1,9 @@
import { Component, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, Component, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@@ -28,6 +29,8 @@ export class DocumentListComponent implements OnInit {
@ViewChild("filterEditor")
private filterEditor: FilterEditorComponent
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
displayMode = 'smallCards' // largeCards, smallCards, details
getTitle() {
@@ -38,6 +41,10 @@ export class DocumentListComponent implements OnInit {
return DOCUMENT_SORT_FIELDS
}
onSort(event: SortEvent) {
this.list.setSort(event.column, event.reverse)
}
get isBulkEditing(): boolean {
return this.list.selected.size > 0
}

View File

@@ -9,10 +9,10 @@
<table class="table table-striped border shadow">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)" i18n>Last correspondence</th>
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="last_correspondence" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Last correspondence</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>

View File

@@ -10,9 +10,9 @@
<table class="table table-striped border shadow">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>

View File

@@ -26,7 +26,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
public collectionSize = 0
public sortField: string
public sortDirection: string
public sortReverse: boolean
getMatching(o: MatchingModel) {
if (o.matching_algorithm == MATCH_AUTO) {
@@ -39,21 +39,8 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
}
onSort(event: SortEvent) {
if (event.direction && event.direction.length > 0) {
this.sortField = event.column
this.sortDirection = event.direction
} else {
this.sortField = null
this.sortDirection = null
}
this.headers.forEach(header => {
if (header.sortable !== this.sortField) {
header.direction = '';
}
});
this.sortField = event.column
this.sortReverse = event.reverse
this.reloadData()
}
@@ -62,8 +49,7 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
}
reloadData() {
// TODO: this is a hack
this.service.list(this.page, null, this.sortField, this.sortDirection == 'des').subscribe(c => {
this.service.list(this.page, null, this.sortField, this.sortReverse).subscribe(c => {
this.data = c.results
this.collectionSize = c.count
});

View File

@@ -36,7 +36,7 @@
<app-input-check i18n-title title="Use system settings" formControlName="darkModeUseSystem" (change)="toggleDarkModeSetting()"></app-input-check>
<div class="custom-control custom-switch" *ngIf="!settingsForm.value.darkModeUseSystem">
<input type="checkbox" class="custom-control-input" id="darkModeEnabled" formControlName="darkModeEnabled" [checked]="settingsForm.value.darkModeEnabled">
<label class="custom-control-label" for="darkModeEnabled">Enabled</label>
<label class="custom-control-label" for="darkModeEnabled" i18n>Enable dark mode</label>
</div>
</div>
</div>
@@ -92,5 +92,5 @@
<div [ngbNavOutlet]="nav" class="border-left border-right border-bottom p-3 mb-3 shadow"></div>
<button type="submit" class="btn btn-primary">Save</button>
<button type="submit" class="btn btn-primary" i18n>Save</button>
</form>

View File

@@ -6,7 +6,7 @@
</button>
</div>
<div class="modal-body">
<app-input-text title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text>
<div class="form-group paperless-input-select">

View File

@@ -10,10 +10,10 @@
<table class="table table-striped border shadow-sm">
<thead>
<tr>
<th scope="col" sortable="name" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" sortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
<th scope="col" i18n>Color</th>
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" sortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
<th scope="col" sortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>

View File

@@ -18,6 +18,8 @@ export const FILTER_MODIFIED_AFTER = 16
export const FILTER_DOES_NOT_HAVE_TAG = 17
export const FILTER_ASN_ISNULL = 18
export const FILTER_RULE_TYPES: FilterRuleType[] = [
{id: FILTER_TITLE, name: "Title contains", filtervar: "title__icontains", datatype: "string", multi: false, default: ""},
@@ -45,6 +47,7 @@ export const FILTER_RULE_TYPES: FilterRuleType[] = [
{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},
{id: FILTER_ASN_ISNULL, name: "ASN is null", filtervar: "archive_serial_number__isnull", datatype: "boolean", multi: false}
]
export interface FilterRuleType {

View File

@@ -1,17 +1,15 @@
import { Directive, EventEmitter, Input, Output } from '@angular/core';
export interface SortEvent {
column: string;
direction: string;
column: string
reverse: boolean
}
const rotate: {[key: string]: string} = { 'asc': 'des', 'des': '', '': 'asc' };
@Directive({
selector: 'th[sortable]',
host: {
'[class.asc]': 'direction === "asc"',
'[class.des]': 'direction === "des"',
'[class.asc]': 'currentSortField == sortable && !currentSortReverse',
'[class.des]': 'currentSortField == sortable && currentSortReverse',
'(click)': 'rotate()'
}
})
@@ -19,12 +17,24 @@ export class SortableDirective {
constructor() { }
@Input() sortable: string = '';
@Input() direction: string = '';
@Input()
sortable: string = '';
@Input()
currentSortReverse: boolean = false
@Input()
currentSortField: string
@Output() sort = new EventEmitter<SortEvent>();
rotate() {
this.direction = rotate[this.direction];
this.sort.emit({column: this.sortable, direction: this.direction});
if (this.currentSortField != this.sortable) {
this.sort.emit({column: this.sortable, reverse: false});
} else if (this.currentSortField == this.sortable && !this.currentSortReverse) {
this.sort.emit({column: this.currentSortField, reverse: true});
} else {
this.sort.emit({column: null, reverse: false});
}
}
}

View File

@@ -111,7 +111,8 @@ export class DocumentListViewService {
this.isReloading = false
},
error => {
if (error.error['detail'] == 'Invalid page.') {
if (this.currentPage != 1 && error.status == 404) {
// this happens when applying a filter: the current page might not be available anymore due to the reduced result set.
this.currentPage = 1
this.reload()
}
@@ -152,6 +153,13 @@ export class DocumentListViewService {
return this.view.sort_reverse
}
setSort(field: string, reverse: boolean) {
this.view.sort_field = field
this.view.sort_reverse = reverse
this.saveDocumentListView()
this.reload()
}
private saveDocumentListView() {
sessionStorage.setItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG, JSON.stringify(this.documentListView))
}
@@ -259,7 +267,7 @@ export class DocumentListViewService {
this.documentListView = null
}
}
if (!this.documentListView || !this.documentListView.filter_rules || !this.documentListView.sort_reverse || !this.documentListView.sort_field) {
if (!this.documentListView || this.documentListView.filter_rules == null || this.documentListView.sort_reverse == null || this.documentListView.sort_field == null) {
this.documentListView = {
filter_rules: [],
sort_reverse: true,

View File

@@ -13,10 +13,10 @@ 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: $localize`Correspondent` },
{ field: "document_type__name", name: $localize`Document type` },
{ field: 'title', name: $localize`Title` },
{ field: 'archive_serial_number', name: $localize`ASN` },
{ field: "correspondent__name", name: $localize`Correspondent` },
{ field: 'title', name: $localize`Title` },
{ field: "document_type__name", name: $localize`Document type` },
{ field: 'created', name: $localize`Created` },
{ field: 'added', name: $localize`Added` },
{ field: 'modified', name: $localize`Modified` }