mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-09 09:58:20 -05:00
added sorting to management pages
This commit is contained in:
parent
2c054621fd
commit
4acea53fae
@ -40,6 +40,7 @@ import { SaveViewConfigDialogComponent } from './components/document-list/save-v
|
|||||||
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
|
||||||
import { DateTimeComponent } from './components/common/input/date-time/date-time.component';
|
import { DateTimeComponent } from './components/common/input/date-time/date-time.component';
|
||||||
import { TagsComponent } from './components/common/input/tags/tags.component';
|
import { TagsComponent } from './components/common/input/tags/tags.component';
|
||||||
|
import { SortableDirective } from './directives/sortable.directive';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -73,7 +74,8 @@ import { TagsComponent } from './components/common/input/tags/tags.component';
|
|||||||
CheckComponent,
|
CheckComponent,
|
||||||
SaveViewConfigDialogComponent,
|
SaveViewConfigDialogComponent,
|
||||||
DateTimeComponent,
|
DateTimeComponent,
|
||||||
TagsComponent
|
TagsComponent,
|
||||||
|
SortableDirective
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
BrowserModule,
|
BrowserModule,
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||||
<th scope="col">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||||
<th scope="col">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||||
<th scope="col">Last correspondence</th>
|
<th scope="col" sortable="last_correspondence" (sort)="onSort($event)">Last correspondence</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -10,9 +10,9 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||||
<th scope="col">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||||
<th scope="col">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Directive, OnInit } from '@angular/core';
|
import { Directive, OnInit, QueryList, ViewChildren } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
import { MatchingModel, MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model';
|
||||||
import { ObjectWithId } from 'src/app/data/object-with-id';
|
import { ObjectWithId } from 'src/app/data/object-with-id';
|
||||||
|
import { SortableDirective, SortEvent } from 'src/app/directives/sortable.directive';
|
||||||
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service';
|
||||||
import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
|
import { DeleteDialogComponent } from '../../common/delete-dialog/delete-dialog.component';
|
||||||
|
|
||||||
@ -14,12 +15,17 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
private editDialogComponent: any) {
|
private editDialogComponent: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>;
|
||||||
|
|
||||||
public data: T[] = []
|
public data: T[] = []
|
||||||
|
|
||||||
public page = 1
|
public page = 1
|
||||||
|
|
||||||
public collectionSize = 0
|
public collectionSize = 0
|
||||||
|
|
||||||
|
public sortField: string
|
||||||
|
public sortDirection: string
|
||||||
|
|
||||||
getMatching(o: MatchingModel) {
|
getMatching(o: MatchingModel) {
|
||||||
if (o.matching_algorithm == MATCH_AUTO) {
|
if (o.matching_algorithm == MATCH_AUTO) {
|
||||||
return "Automatic"
|
return "Automatic"
|
||||||
@ -30,12 +36,31 @@ export abstract class GenericListComponent<T extends ObjectWithId> implements On
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSort(event: SortEvent) {
|
||||||
|
|
||||||
|
if (event.direction && event.direction.length > 0) {
|
||||||
|
this.sortField = event.column
|
||||||
|
this.sortDirection = event.direction
|
||||||
|
} else {
|
||||||
|
this.sortField = null
|
||||||
|
this.sortDirection = null
|
||||||
|
}
|
||||||
|
|
||||||
|
this.headers.forEach(header => {
|
||||||
|
if (header.sortable !== this.sortField) {
|
||||||
|
header.direction = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.reloadData()
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.reloadData()
|
this.reloadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadData() {
|
reloadData() {
|
||||||
this.service.list(this.page).subscribe(c => {
|
this.service.list(this.page, null, this.sortField, this.sortDirection).subscribe(c => {
|
||||||
this.data = c.results
|
this.data = c.results
|
||||||
this.collectionSize = c.count
|
this.collectionSize = c.count
|
||||||
});
|
});
|
||||||
|
@ -9,10 +9,10 @@
|
|||||||
<table class="table table-striped">
|
<table class="table table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Name</th>
|
<th scope="col" sortable="name" (sort)="onSort($event)">Name</th>
|
||||||
<th scope="col">Colour</th>
|
<th scope="col">Colour</th>
|
||||||
<th scope="col">Matching</th>
|
<th scope="col" sortable="matching_algorithm" (sort)="onSort($event)">Matching</th>
|
||||||
<th scope="col">Document count</th>
|
<th scope="col" sortable="document_count" (sort)="onSort($event)">Document count</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
8
src-ui/src/app/directives/sortable.directive.spec.ts
Normal file
8
src-ui/src/app/directives/sortable.directive.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { SortableDirective } from './sortable.directive';
|
||||||
|
|
||||||
|
describe('SortableDirective', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new SortableDirective();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
30
src-ui/src/app/directives/sortable.directive.ts
Normal file
30
src-ui/src/app/directives/sortable.directive.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { Directive, EventEmitter, Input, Output } from '@angular/core';
|
||||||
|
|
||||||
|
export interface SortEvent {
|
||||||
|
column: string;
|
||||||
|
direction: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotate: {[key: string]: string} = { 'asc': 'des', 'des': '', '': 'asc' };
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: 'th[sortable]',
|
||||||
|
host: {
|
||||||
|
'[class.asc]': 'direction === "asc"',
|
||||||
|
'[class.des]': 'direction === "des"',
|
||||||
|
'(click)': 'rotate()'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
export class SortableDirective {
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
@Input() sortable: string = '';
|
||||||
|
@Input() direction: string = '';
|
||||||
|
@Output() sort = new EventEmitter<SortEvent>();
|
||||||
|
|
||||||
|
rotate() {
|
||||||
|
this.direction = rotate[this.direction];
|
||||||
|
this.sort.emit({column: this.sortable, direction: this.direction});
|
||||||
|
}
|
||||||
|
}
|
@ -21,7 +21,17 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
list(page?: number, pageSize?: number, ordering?: string, extraParams?): Observable<Results<T>> {
|
private getOrderingQueryParam(sortField: string, sortDirection: string) {
|
||||||
|
if (sortField && sortDirection) {
|
||||||
|
return (sortDirection == 'des' ? '-' : '') + sortField
|
||||||
|
} else if (sortField) {
|
||||||
|
return sortField
|
||||||
|
} else {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, extraParams?): Observable<Results<T>> {
|
||||||
let httpParams = new HttpParams()
|
let httpParams = new HttpParams()
|
||||||
if (page) {
|
if (page) {
|
||||||
httpParams = httpParams.set('page', page.toString())
|
httpParams = httpParams.set('page', page.toString())
|
||||||
@ -29,6 +39,7 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
|
|||||||
if (pageSize) {
|
if (pageSize) {
|
||||||
httpParams = httpParams.set('page_size', pageSize.toString())
|
httpParams = httpParams.set('page_size', pageSize.toString())
|
||||||
}
|
}
|
||||||
|
let ordering = this.getOrderingQueryParam(sortField, sortDirection)
|
||||||
if (ordering) {
|
if (ordering) {
|
||||||
httpParams = httpParams.set('ordering', ordering)
|
httpParams = httpParams.set('ordering', ordering)
|
||||||
}
|
}
|
||||||
|
@ -47,16 +47,8 @@ export class DocumentService extends AbstractPaperlessService<PaperlessDocument>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getOrderingQueryParam(sortField: string, sortDirection: string) {
|
|
||||||
if (DOCUMENT_SORT_FIELDS.find(f => f.field == sortField)) {
|
|
||||||
return (sortDirection == SORT_DIRECTION_DESCENDING ? '-' : '') + sortField
|
|
||||||
} else {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> {
|
list(page?: number, pageSize?: number, sortField?: string, sortDirection?: string, filterRules?: FilterRule[]): Observable<Results<PaperlessDocument>> {
|
||||||
return super.list(page, pageSize, this.getOrderingQueryParam(sortField, sortDirection), this.filterRulesToQueryParams(filterRules))
|
return super.list(page, pageSize, sortField, sortDirection, this.filterRulesToQueryParams(filterRules))
|
||||||
}
|
}
|
||||||
|
|
||||||
getPreviewUrl(id: number): string {
|
getPreviewUrl(id: number): string {
|
||||||
|
@ -28,4 +28,34 @@ body {
|
|||||||
.form-control-dark:focus {
|
.form-control-dark:focus {
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.asc {
|
||||||
|
background-color: #f8f9fa!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.asc:after {
|
||||||
|
content: '';
|
||||||
|
transform: rotate(180deg);
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC") no-repeat;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
display: block;
|
||||||
|
background-size: 1rem;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.des {
|
||||||
|
background-color: #f8f9fa!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.des:after {
|
||||||
|
content: '';
|
||||||
|
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC") no-repeat;
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
display: block;
|
||||||
|
background-size: 1rem;
|
||||||
|
float: right;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user