mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge branch 'dev' into feature-bulk-edit
This commit is contained in:
		| @@ -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] | ||||
| }) | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 | ||||
|     ) { | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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: [{ | ||||
|   | ||||
| @@ -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'; | ||||
|  | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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 { | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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({ | ||||
|   | ||||
| @@ -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) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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 | ||||
|     }) | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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({ | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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({ | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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() | ||||
|         } | ||||
|       ) | ||||
|     })     | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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> | ||||
| @@ -0,0 +1,7 @@ | ||||
| .date-filter { | ||||
|   min-width: 250px; | ||||
|  | ||||
|   .btn-link { | ||||
|     line-height: 1; | ||||
|   } | ||||
| } | ||||
| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
| @@ -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}) | ||||
|   } | ||||
| } | ||||
| @@ -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> | ||||
| @@ -0,0 +1,4 @@ | ||||
| .selected-icon { | ||||
|   min-width: 1em; | ||||
|   min-height: 1em; | ||||
| } | ||||
| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
| @@ -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) | ||||
|   } | ||||
| } | ||||
| @@ -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> | ||||
| @@ -0,0 +1,8 @@ | ||||
| .dropdown-menu { | ||||
|   min-width: 250px; | ||||
|  | ||||
|   .items { | ||||
|     max-height: 400px; | ||||
|     overflow-y: scroll; | ||||
|   } | ||||
| } | ||||
| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
| @@ -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() | ||||
|   } | ||||
| } | ||||
| @@ -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> | ||||
|   | ||||
| @@ -0,0 +1,10 @@ | ||||
| .quick-filter { | ||||
|   min-width: 250px; | ||||
|   max-height: 400px; | ||||
|   overflow-y: scroll; | ||||
|  | ||||
|   .selected-icon { | ||||
|     min-width: 1em; | ||||
|     min-height: 1em; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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}`) | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
| @@ -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}`) | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     }); | ||||
|   | ||||
| @@ -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) | ||||
|     }) | ||||
|   } | ||||
|   | ||||
| @@ -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> | ||||
|   | ||||
| @@ -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() | ||||
|     }) | ||||
|  | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -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) | ||||
|   } | ||||
|   | ||||
| @@ -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}`) | ||||
|     }) | ||||
|  | ||||
|   } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
							
								
								
									
										18
									
								
								src-ui/src/app/data/paperless-saved-view.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src-ui/src/app/data/paperless-saved-view.ts
									
									
									
									
									
										Normal 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[] | ||||
|  | ||||
| } | ||||
| @@ -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 | ||||
|  | ||||
| } | ||||
							
								
								
									
										17
									
								
								src-ui/src/app/pipes/filter.pipe.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src-ui/src/app/pipes/filter.pipe.ts
									
									
									
									
									
										Normal 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()); | ||||
|       }); | ||||
|     }); | ||||
|    } | ||||
| } | ||||
| @@ -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' | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -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) | ||||
|   } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										16
									
								
								src-ui/src/app/services/rest/saved-view.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src-ui/src/app/services/rest/saved-view.service.spec.ts
									
									
									
									
									
										Normal 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(); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										59
									
								
								src-ui/src/app/services/rest/saved-view.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src-ui/src/app/services/rest/saved-view.service.ts
									
									
									
									
									
										Normal 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()) | ||||
|     ) | ||||
|   } | ||||
| } | ||||
| @@ -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(); | ||||
|   }); | ||||
| }); | ||||
| @@ -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() | ||||
|     } | ||||
|  | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 jonaswinkler
					jonaswinkler