mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Completely refactored because programming
Extracted filter editor to service Made all components actually reactive
This commit is contained in:
		| @@ -29,6 +29,7 @@ import { AppFrameComponent } from './components/app-frame/app-frame.component'; | |||||||
| import { ToastsComponent } from './components/common/toasts/toasts.component'; | import { ToastsComponent } from './components/common/toasts/toasts.component'; | ||||||
| import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; | import { FilterEditorComponent } from './components/filter-editor/filter-editor.component'; | ||||||
| import { FilterDropdownComponent } from './components/filter-editor/filter-dropdown/filter-dropdown.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 { 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 { 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 { DocumentCardSmallComponent } from './components/document-list/document-card-small/document-card-small.component'; | ||||||
| @@ -77,6 +78,7 @@ import { FilterPipe } from './pipes/filter.pipe'; | |||||||
|     ToastsComponent, |     ToastsComponent, | ||||||
|     FilterEditorComponent, |     FilterEditorComponent, | ||||||
|     FilterDropdownComponent, |     FilterDropdownComponent, | ||||||
|  |     FilterDropdownButtonComponent, | ||||||
|     FilterDropdownDateComponent, |     FilterDropdownDateComponent, | ||||||
|     DocumentCardLargeComponent, |     DocumentCardLargeComponent, | ||||||
|     DocumentCardSmallComponent, |     DocumentCardSmallComponent, | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ | |||||||
|  |  | ||||||
| <div class="card w-100 mb-3"> | <div class="card w-100 mb-3"> | ||||||
|   <div class="card-body"> |   <div class="card-body"> | ||||||
|     <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> |     <app-filter-editor [(filterEditorService)]="filterEditorService" (apply)="applyFilterRules()" (clear)="clearFilterRules()" #filterEditor></app-filter-editor> | ||||||
|   </div> |   </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ 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 { 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 { SavedViewConfig } from 'src/app/data/saved-view-config'; | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | ||||||
|  | import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service'; | ||||||
| import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; | import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; | ||||||
| import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; | import { SavedViewConfigService } from 'src/app/services/saved-view-config.service'; | ||||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||||
| @@ -26,6 +27,7 @@ export class DocumentListComponent implements OnInit { | |||||||
|   constructor( |   constructor( | ||||||
|     public list: DocumentListViewService, |     public list: DocumentListViewService, | ||||||
|     public savedViewConfigService: SavedViewConfigService, |     public savedViewConfigService: SavedViewConfigService, | ||||||
|  |     public filterEditorService: FilterEditorViewService, | ||||||
|     public route: ActivatedRoute, |     public route: ActivatedRoute, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     public modalService: NgbModal, |     public modalService: NgbModal, | ||||||
| @@ -33,14 +35,18 @@ export class DocumentListComponent implements OnInit { | |||||||
|  |  | ||||||
|   displayMode = 'smallCards' // largeCards, smallCards, details |   displayMode = 'smallCards' // largeCards, smallCards, details | ||||||
|  |  | ||||||
|   filterRules: FilterRule[] = [] |  | ||||||
|  |  | ||||||
|   @ViewChild('filterEditor') filterEditor: FilterEditorComponent |  | ||||||
|  |  | ||||||
|   get isFiltered() { |   get isFiltered() { | ||||||
|     return this.list.filterRules?.length > 0 |     return this.list.filterRules?.length > 0 | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   set filterRules(filterRules: FilterRule[]) { | ||||||
|  |     this.filterEditorService.filterRules = filterRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get filterRules(): FilterRule[] { | ||||||
|  |     return this.filterEditorService.filterRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|   getTitle() { |   getTitle() { | ||||||
|     return this.list.savedViewTitle || "Documents" |     return this.list.savedViewTitle || "Documents" | ||||||
|   } |   } | ||||||
| @@ -60,28 +66,29 @@ export class DocumentListComponent implements OnInit { | |||||||
|     this.route.paramMap.subscribe(params => { |     this.route.paramMap.subscribe(params => { | ||||||
|       if (params.has('id')) { |       if (params.has('id')) { | ||||||
|         this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) |         this.list.savedView = this.savedViewConfigService.getConfig(params.get('id')) | ||||||
|         this.filterRules = this.list.filterRules |         this.filterEditorService.filterRules = this.list.filterRules | ||||||
|         this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) |         this.titleService.setTitle(`${this.list.savedView.title} - ${environment.appTitle}`) | ||||||
|       } else { |       } else { | ||||||
|         this.list.savedView = null |         this.list.savedView = null | ||||||
|         this.filterRules = this.list.filterRules |         this.filterEditorService.filterRules = this.list.filterRules | ||||||
|         this.titleService.setTitle(`Documents - ${environment.appTitle}`) |         this.titleService.setTitle(`Documents - ${environment.appTitle}`) | ||||||
|       } |       } | ||||||
|       this.list.clear() |       this.list.clear() | ||||||
|       this.list.reload() |       this.list.reload() | ||||||
|     }) |     }) | ||||||
|  |     this.filterEditorService.filterRules = this.list.filterRules | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyFilterRules() { |   applyFilterRules() { | ||||||
|     this.list.filterRules = this.filterRules |     this.list.filterRules = this.filterEditorService.filterRules | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clearFilterRules() { |   clearFilterRules() { | ||||||
|     this.list.filterRules = this.filterRules |     this.list.filterRules = this.filterEditorService.filterRules | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   loadViewConfig(config: SavedViewConfig) { |   loadViewConfig(config: SavedViewConfig) { | ||||||
|     this.filterRules = cloneFilterRules(config.filterRules) |     this.filterEditorService.filterRules = cloneFilterRules(config.filterRules) | ||||||
|     this.list.load(config) |     this.list.load(config) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -106,15 +113,18 @@ export class DocumentListComponent implements OnInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickTag(tagID: number) { |   clickTag(tagID: number) { | ||||||
|     this.filterEditor.toggleFilterByItem(tagID, FILTER_HAS_TAG) |     this.filterEditorService.toggleFitlerByTagID(tagID) | ||||||
|  |     this.applyFilterRules() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickCorrespondent(correspondentID: number) { |   clickCorrespondent(correspondentID: number) { | ||||||
|     this.filterEditor.toggleFilterByItem(correspondentID, FILTER_CORRESPONDENT) |     this.filterEditorService.toggleFitlerByCorrespondentID(correspondentID) | ||||||
|  |     this.applyFilterRules() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clickDocumentType(documentTypeID: number) { |   clickDocumentType(documentTypeID: number) { | ||||||
|     this.filterEditor.toggleFilterByItem(documentTypeID, FILTER_DOCUMENT_TYPE) |     this.filterEditorService.toggleFitlerByDocumentTypeID(documentTypeID) | ||||||
|  |     this.applyFilterRules() | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
|         <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> |         <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> | ||||||
|           <div class="mb-1"><small>Before</small></div> |           <div class="mb-1"><small>Before</small></div> | ||||||
|           <div class="input-group input-group-sm"> |           <div class="input-group input-group-sm"> | ||||||
|             <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="dateBefore" ngbDatepicker (dateSelect)="dateSelected($event)" #dpBefore="ngbDatepicker"> |             <input class="form-control" type="text" placeholder="yyyy-mm-dd" name="before" [(ngModel)]="_dateBefore" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpBefore="ngbDatepicker"> | ||||||
|             <div class="input-group-append"> |             <div class="input-group-append"> | ||||||
|               <button class="btn btn-outline-secondary btn-sm" (click)="dpBefore.toggle()" type="button"> |               <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"> |                 <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
| @@ -25,7 +25,7 @@ | |||||||
|         <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> |         <div class="list-group-item d-flex flex-column align-items-start" role="menuitem"> | ||||||
|           <div class="mb-1"><small>After</small></div> |           <div class="mb-1"><small>After</small></div> | ||||||
|           <div class="input-group"> |           <div class="input-group"> | ||||||
|             <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="dateAfter" ngbDatepicker (dateSelect)="dateSelected($event)" #dpAfter="ngbDatepicker"> |             <input class="form-control form-control-sm" type="text" placeholder="yyyy-mm-dd" name="after" [(ngModel)]="_dateAfter" ngbDatepicker (dateSelect)="onDateSelected($event)" #dpAfter="ngbDatepicker"> | ||||||
|             <div class="input-group-append"> |             <div class="input-group-append"> | ||||||
|               <button class="btn btn-outline-secondary btn-sm" (click)="dpAfter.toggle()" type="button"> |               <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"> |                 <svg width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-calendar-date" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; | import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; | ||||||
| import { FilterRule } from 'src/app/data/filter-rule'; | import { FilterRule } from 'src/app/data/filter-rule'; | ||||||
| import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; |  | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
| import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; | import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; | ||||||
|  |  | ||||||
| @@ -12,23 +11,25 @@ import { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; | |||||||
| export class FilterDropdownDateComponent { | export class FilterDropdownDateComponent { | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   filterRuleTypeIDs: number[] = [] |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   selected = new EventEmitter() |  | ||||||
|  |  | ||||||
|   filterRuleTypes: FilterRuleType[] = [] |  | ||||||
|   title: string |  | ||||||
|   dateAfter: NgbDateStruct |  | ||||||
|   dateBefore: NgbDateStruct |   dateBefore: NgbDateStruct | ||||||
|  |  | ||||||
|   ngOnInit(): void { |   @Input() | ||||||
|     this.filterRuleTypes = this.filterRuleTypeIDs.map(id => FILTER_RULE_TYPES.find(rt => rt.id == id)) |   dateAfter: NgbDateStruct | ||||||
|     this.title = this.filterRuleTypes[0].displayName |  | ||||||
|   } |   @Input() | ||||||
|  |   title: string | ||||||
|  |  | ||||||
|  |   @Output() | ||||||
|  |   dateBeforeSet = new EventEmitter() | ||||||
|  |  | ||||||
|  |   @Output() | ||||||
|  |   dateAfterSet = new EventEmitter() | ||||||
|  |  | ||||||
|  |   _dateBefore: NgbDateStruct | ||||||
|  |   _dateAfter: NgbDateStruct | ||||||
|  |  | ||||||
|   setDateQuickFilter(range: any) { |   setDateQuickFilter(range: any) { | ||||||
|     this.dateAfter = this.dateBefore = undefined |     this._dateAfter = this._dateBefore = undefined | ||||||
|     let date = new Date() |     let date = new Date() | ||||||
|     let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } |     let newDate: NgbDateStruct = { year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate() } | ||||||
|     switch (typeof range) { |     switch (typeof range) { | ||||||
| @@ -47,17 +48,12 @@ export class FilterDropdownDateComponent { | |||||||
|       default: |       default: | ||||||
|         break |         break | ||||||
|     } |     } | ||||||
|     this.dateAfter = newDate |     this._dateAfter = newDate | ||||||
|     this.dateSelected(this.dateAfter) |     this.onDateSelected(this._dateAfter) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   dateSelected(date:NgbDateStruct) { |   onDateSelected(date:NgbDateStruct) { | ||||||
|     let isAfter = NgbDate.from(this.dateAfter).equals(date) |     let emitter = this._dateAfter && NgbDate.from(this._dateAfter).equals(date) ? this.dateAfterSet : this.dateBeforeSet | ||||||
|  |     emitter.emit(date) | ||||||
|     let filterRuleType = this.filterRuleTypes.find(rt => rt.filtervar.indexOf(isAfter ? 'gt' : 'lt') > -1) |  | ||||||
|     if (filterRuleType) { |  | ||||||
|       let dateFilterRule:FilterRule = {value: `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}`, type: filterRuleType} |  | ||||||
|       this.selected.emit(dateFilterRule) |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,12 @@ | |||||||
|  | <button class="list-group-item list-group-item-action d-flex align-items-center" 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="display == 'tag'; 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 bg-primary text-light rounded-pill ml-auto">{{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 } 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 { | ||||||
|  |  | ||||||
|  |   constructor() { } | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   display: string | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   selected: boolean | ||||||
|  |  | ||||||
|  |   @Output() | ||||||
|  |   toggle = new EventEmitter() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |   toggleItem(): void { | ||||||
|  |     this.selected = !this.selected | ||||||
|  |     this.toggle.emit(this.item) | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,19 +3,10 @@ | |||||||
|   <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> |   <div class="dropdown-menu quick-filter shadow" ngbDropdownMenu attr.aria-labelledby="dropdown{{title}}"> | ||||||
|     <div class="list-group list-group-flush"> |     <div class="list-group list-group-flush"> | ||||||
|       <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> |       <input class="list-group-item form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> | ||||||
|       <ng-container *ngIf="(items | filter: filterText).length > 0"> |       <ng-container *ngIf="(items$ | async)?.results as items"> | ||||||
|         <button class="list-group-item list-group-item-action d-flex align-items-center" role="menuitem" *ngFor="let item of items | filter: filterText; let i = index" (click)="toggleItem(item)"> |         <ng-container *ngFor="let item of items | filter: filterText; let i = index"> | ||||||
|           <div class="selected-icon mr-1"> |           <app-filter-dropdown-button [item]="item" [display]="display" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filter-dropdown-button> | ||||||
|             <svg *ngIf="itemsActive.includes(item)" width="1em" height="1em" viewBox="0 0 16 16" class="bi bi-check" fill="currentColor" xmlns="http://www.w3.org/2000/svg"> |         </ng-container> | ||||||
|               <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="display == 'tag'; 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 bg-primary text-light rounded-pill ml-auto">{{item.document_count}}</div> |  | ||||||
|         </button> |  | ||||||
|       </ng-container> |       </ng-container> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -2,9 +2,4 @@ | |||||||
|   min-width: 250px; |   min-width: 250px; | ||||||
|   max-height: 400px; |   max-height: 400px; | ||||||
|   overflow-y: scroll; |   overflow-y: scroll; | ||||||
|  |  | ||||||
|   .selected-icon { |  | ||||||
|     min-width: 1em; |  | ||||||
|     min-height: 1em; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; | import { Component, EventEmitter, Input, OnInit, Output, ElementRef, ViewChild } from '@angular/core'; | ||||||
| import { FilterRuleType, FILTER_RULE_TYPES } from 'src/app/data/filter-rule-type'; | import { Observable } from 'rxjs'; | ||||||
|  | import { Results } from 'src/app/data/results'; | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
| import { FilterPipe } from  'src/app/pipes/filter.pipe'; | import { FilterPipe } from  'src/app/pipes/filter.pipe'; | ||||||
|  |  | ||||||
| @@ -13,29 +14,37 @@ export class FilterDropdownComponent implements OnInit { | |||||||
|   constructor(private filterPipe: FilterPipe) { } |   constructor(private filterPipe: FilterPipe) { } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   filterRuleTypeID: number |   items$: Observable<Results<ObjectWithId>> | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   itemsSelected: ObjectWithId[] | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   title: string | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   display: string | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   toggle = new EventEmitter() |   toggle = new EventEmitter() | ||||||
|  |  | ||||||
|   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef |   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef | ||||||
|  |  | ||||||
|   items: ObjectWithId[] = [] |  | ||||||
|   itemsActive: ObjectWithId[] = [] |  | ||||||
|   title: string |  | ||||||
|   filterText: string |   filterText: string | ||||||
|   display: string |   items: ObjectWithId[] | ||||||
|  |  | ||||||
|   ngOnInit(): void { |   ngOnInit() { | ||||||
|     let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == this.filterRuleTypeID) |     this.items$.subscribe(result => this.items = result.results) | ||||||
|     this.title = filterRuleType.displayName |  | ||||||
|     this.display = filterRuleType.datatype |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   toggleItem(item: ObjectWithId): void { |   toggleItem(item: ObjectWithId): void { | ||||||
|     this.toggle.emit(item) |     this.toggle.emit(item) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   isItemSelected(item: ObjectWithId): boolean { | ||||||
|  |     return this.itemsSelected?.find(i => i.id == item.id) !== undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|   dropdownOpenChange(open: boolean): void { |   dropdownOpenChange(open: boolean): void { | ||||||
|     if (open) { |     if (open) { | ||||||
|       setTimeout(() => { |       setTimeout(() => { | ||||||
|   | |||||||
| @@ -3,14 +3,17 @@ | |||||||
|     <div class="text-muted mt-1">Filter by:</div> |     <div class="text-muted mt-1">Filter by:</div> | ||||||
|   </div> |   </div> | ||||||
|   <div class="col"> |   <div class="col"> | ||||||
|     <input class="form-control form-control-sm" type="text" [(ngModel)]="filterText" placeholder="Title" #filterTextInput> |     <input class="form-control form-control-sm" type="text" [(ngModel)]="filterEditorService.filterText" placeholder="Title" #filterTextInput> | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|   <app-filter-dropdown class="col-auto" *ngFor="let quickFilterRuleTypeID of quickFilterRuleTypeIDs" [filterRuleTypeID]="quickFilterRuleTypeID" (toggle)="toggleFilterByItem($event, quickFilterRuleTypeID)"></app-filter-dropdown> |   <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.tags$" [itemsSelected]="filterEditorService.selectedTags" [title]="'Tags'" [display]="'tag'" (toggle)="onToggleTag($event)"></app-filter-dropdown> | ||||||
|  |   <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.correspondents$" [itemsSelected]="filterEditorService.selectedCorrespondents" [title]="'Correspondents'" (toggle)="onToggleCorrespondent($event)"></app-filter-dropdown> | ||||||
|  |   <app-filter-dropdown class="col-auto" [(items$)]="filterEditorService.documentTypes$" [itemsSelected]="filterEditorService.selectedDocumentTypes" [title]="'Document Types'" (toggle)="onToggleDocumentType($event)"></app-filter-dropdown> | ||||||
|  |  | ||||||
|   <app-filter-dropdown-date class="col-auto" *ngFor="let dateAddedFilterRuleTypeID of dateAddedFilterRuleTypeIDs" [filterRuleTypeIDs]="dateAddedFilterRuleTypeID" (selected)="setDateFilter($event)"></app-filter-dropdown-date> |   <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateCreatedBefore" [dateAfter]="filterEditorService.dateCreatedAfter" [title]="'Created'" (dateBeforeSet)="onDateCreatedBeforeSet($event)" (dateAfterSet)="onDateCreatedAfterSet($event)"></app-filter-dropdown-date> | ||||||
|  |   <app-filter-dropdown-date class="col-auto" [dateBefore]="filterEditorService.dateAddedBefore" [dateAfter]="filterEditorService.dateAddedAfter" [title]="'Added'"  (dateBeforeSet)="onDateAddedBeforeSet($event)" (dateAfterSet)="onDateAddedAfterSet($event)"></app-filter-dropdown-date> | ||||||
|  |  | ||||||
|   <button class="btn btn-link btn-sm" [disabled]="!hasFilters()" (click)="clearSelected()"> |   <button class="btn btn-link btn-sm" [disabled]="!filterEditorService.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"> |     <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"/> |       <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> |     </svg> | ||||||
|   | |||||||
| @@ -1,14 +1,10 @@ | |||||||
| import { Component, EventEmitter, Input, OnInit, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; | import { Component, EventEmitter, Input, Output, ElementRef, AfterViewInit, QueryList, ViewChild, ViewChildren } from '@angular/core'; | ||||||
| import { FilterRule } from 'src/app/data/filter-rule'; | import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; | ||||||
| import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
|  | import { FilterEditorViewService } from 'src/app/services/filter-editor-view.service' | ||||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service'; |  | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; |  | ||||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; |  | ||||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; |  | ||||||
| import { TagService } from 'src/app/services/rest/tag.service'; |  | ||||||
| import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' | import { FilterDropdownComponent } from './filter-dropdown/filter-dropdown.component' | ||||||
| import { FilterDropdownDateComponent } from './filter-dropdown-date/filter-dropdown-date.component' | import { FilterDropdownDateComponent } from './filter-dropdown-date/filter-dropdown-date.component' | ||||||
| import { fromEvent } from 'rxjs'; | import { fromEvent } from 'rxjs'; | ||||||
| @@ -20,38 +16,20 @@ import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; | |||||||
|   templateUrl: './filter-editor.component.html', |   templateUrl: './filter-editor.component.html', | ||||||
|   styleUrls: ['./filter-editor.component.scss'] |   styleUrls: ['./filter-editor.component.scss'] | ||||||
| }) | }) | ||||||
| export class FilterEditorComponent implements OnInit, AfterViewInit { | export class FilterEditorComponent implements AfterViewInit { | ||||||
|  |  | ||||||
|   constructor(private documentTypeService: DocumentTypeService, private tagService: TagService, private correspondentService: CorrespondentService) { } |   constructor() { } | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   filterEditorService: FilterEditorViewService | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   clear = new EventEmitter() |   clear = new EventEmitter() | ||||||
|  |  | ||||||
|   @Input() |  | ||||||
|   filterRules: FilterRule[] = [] |  | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   apply = new EventEmitter() |   apply = new EventEmitter() | ||||||
|  |  | ||||||
|   @ViewChild('filterTextInput') filterTextInput: ElementRef; |   @ViewChild('filterTextInput') filterTextInput: ElementRef; | ||||||
|   @ViewChildren(FilterDropdownComponent) quickFilterDropdowns!: QueryList<FilterDropdownComponent>; |  | ||||||
|   @ViewChildren(FilterDropdownDateComponent) quickDateFilterDropdowns!: QueryList<FilterDropdownDateComponent>; |  | ||||||
|  |  | ||||||
|   quickFilterRuleTypeIDs: number[] = [FILTER_HAS_TAG, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE] |  | ||||||
|   dateAddedFilterRuleTypeIDs: any[] = [[FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER], [FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER]] |  | ||||||
|  |  | ||||||
|   correspondents: PaperlessCorrespondent[] = [] |  | ||||||
|   tags: PaperlessTag[] = [] |  | ||||||
|   documentTypes: PaperlessDocumentType[] = [] |  | ||||||
|  |  | ||||||
|   filterText: string |  | ||||||
|  |  | ||||||
|   ngOnInit(): void { |  | ||||||
|     this.updateTextFilterInput() |  | ||||||
|     this.tagService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_HAS_TAG)) |  | ||||||
|     this.correspondentService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_CORRESPONDENT)) |  | ||||||
|     this.documentTypeService.listAll().subscribe(result => this.setDropdownItems(result.results, FILTER_DOCUMENT_TYPE)) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ngAfterViewInit() { |   ngAfterViewInit() { | ||||||
|     fromEvent(this.filterTextInput.nativeElement,'keyup').pipe( |     fromEvent(this.filterTextInput.nativeElement,'keyup').pipe( | ||||||
| @@ -59,120 +37,52 @@ export class FilterEditorComponent implements OnInit, AfterViewInit { | |||||||
|       distinctUntilChanged(), |       distinctUntilChanged(), | ||||||
|       tap() |       tap() | ||||||
|     ).subscribe((event: Event) => { |     ).subscribe((event: Event) => { | ||||||
|       this.filterText = (event.target as HTMLInputElement).value |       this.filterEditorService.filterText = (event.target as HTMLInputElement).value | ||||||
|       this.onTextFilterInput() |       this.applyFilters() | ||||||
|     }) |     }) | ||||||
|     this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   setDropdownItems(items: ObjectWithId[], filterRuleTypeID: number): void { |   applyFilters() { | ||||||
|     let dropdown: FilterDropdownComponent = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) |  | ||||||
|     if (dropdown) { |  | ||||||
|       dropdown.items = items |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   updateDropdownActiveItems(dropdown: FilterDropdownComponent): void { |  | ||||||
|     let activeRulesValues = this.filterRules.filter(r => r.type.id == dropdown.filterRuleTypeID).map(r => r.value) |  | ||||||
|     let activeItems = [] |  | ||||||
|     if (activeRulesValues.length > 0) { |  | ||||||
|       activeItems = dropdown.items.filter(i => activeRulesValues.includes(i.id)) |  | ||||||
|     } |  | ||||||
|     dropdown.itemsActive = activeItems |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   updateDateDropdown(dateDropdown: FilterDropdownDateComponent) { |  | ||||||
|     let activeRules = this.filterRules.filter(r => dateDropdown.filterRuleTypeIDs.includes(r.type.id)) |  | ||||||
|     if (activeRules.length > 0) { |  | ||||||
|       activeRules.forEach(rule => { |  | ||||||
|         let date = { year: rule.value.substring(0,4), month: rule.value.substring(5,7), day: rule.value.substring(8,10) } |  | ||||||
|         rule.type.filtervar.indexOf('gt') > -1 ? dateDropdown.dateAfter = date : dateDropdown.dateBefore = date |  | ||||||
|       }) |  | ||||||
|     } else { |  | ||||||
|       dateDropdown.dateAfter = dateDropdown.dateBefore = undefined |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   getDropdownByFilterRuleTypeID(filterRuleTypeID: number): FilterDropdownComponent { |  | ||||||
|     return this.quickFilterDropdowns.find(d => d.filterRuleTypeID == filterRuleTypeID) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   applySelected() { |  | ||||||
|     this.apply.next() |     this.apply.next() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   clearSelected() { |   clearSelected() { | ||||||
|     this.filterRules.splice(0,this.filterRules.length) |     this.filterEditorService.clear() | ||||||
|     this.updateTextFilterInput() |  | ||||||
|     this.quickFilterDropdowns.forEach(d => this.updateDropdownActiveItems(d)) |  | ||||||
|     this.quickDateFilterDropdowns.forEach(d => this.updateDateDropdown(d)) |  | ||||||
|     this.clear.next() |     this.clear.next() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   hasFilters() { |   onToggleTag(tag: PaperlessTag) { | ||||||
|     return this.filterRules.length > 0 |     this.filterEditorService.toggleFitlerByTag(tag) | ||||||
|  |     this.applyFilters() | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   updateTextFilterInput() { |   onToggleCorrespondent(correspondent: PaperlessCorrespondent) { | ||||||
|     let existingTextRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) |     this.filterEditorService.toggleFitlerByCorrespondent(correspondent) | ||||||
|     if (existingTextRule) this.filterText = existingTextRule.value |     this.applyFilters() | ||||||
|     else this.filterText = '' |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   onTextFilterInput() { |   onToggleDocumentType(documentType: PaperlessDocumentType) { | ||||||
|     let text = this.filterText |     this.filterEditorService.toggleFitlerByDocumentType(documentType) | ||||||
|     let filterRules = this.filterRules |     this.applyFilters() | ||||||
|     let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) |  | ||||||
|     if (existingRule && existingRule.value == text) { |  | ||||||
|       return |  | ||||||
|     } else if (existingRule) { |  | ||||||
|       existingRule.value = text |  | ||||||
|     } else { |  | ||||||
|       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) |  | ||||||
|     } |  | ||||||
|     this.filterRules = filterRules |  | ||||||
|     this.applySelected() |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   toggleFilterByItem(item: any, filterRuleTypeID: number) { |   onDateCreatedBeforeSet(date: NgbDateStruct) { | ||||||
|     let dropdown = this.getDropdownByFilterRuleTypeID(filterRuleTypeID) |     this.filterEditorService.setDateCreatedBefore(date) | ||||||
|     if (typeof item == 'number') { |     this.applyFilters() | ||||||
|       item = dropdown.items.find(i => i.id == item) |  | ||||||
|     } |  | ||||||
|     let filterRules = this.filterRules |  | ||||||
|     let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) |  | ||||||
|     let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) |  | ||||||
|  |  | ||||||
|     if (existingRule && existingRule.value == item.id) { |  | ||||||
|       filterRules.splice(filterRules.indexOf(existingRule), 1) |  | ||||||
|     } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { |  | ||||||
|       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) |  | ||||||
|     } else if (existingRule && existingRule.value == item.id) { |  | ||||||
|       return |  | ||||||
|     } else if (existingRule) { |  | ||||||
|       existingRule.value = item.id |  | ||||||
|     } else { |  | ||||||
|       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.updateDropdownActiveItems(dropdown) |  | ||||||
|  |  | ||||||
|     this.filterRules = filterRules |  | ||||||
|     this.applySelected() |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   setDateFilter(newFilterRule: FilterRule) { |   onDateCreatedAfterSet(date: NgbDateStruct) { | ||||||
|     let filterRules = this.filterRules |     this.filterEditorService.setDateCreatedAfter(date) | ||||||
|     let existingRule = filterRules.find(rule => rule.type.id == newFilterRule.type.id) |     this.applyFilters() | ||||||
|  |  | ||||||
|     if (existingRule) { |  | ||||||
|       existingRule.value = newFilterRule.value |  | ||||||
|     } else { |  | ||||||
|       filterRules.push(newFilterRule) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     this.filterRules = filterRules |  | ||||||
|     this.applySelected() |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   onDateAddedBeforeSet(date: NgbDateStruct) { | ||||||
|  |     this.filterEditorService.setDateAddedBefore(date) | ||||||
|  |     this.applyFilters() | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   onDateAddedAfterSet(date: NgbDateStruct) { | ||||||
|  |     this.filterEditorService.setDateAddedAfter(date) | ||||||
|  |     this.applyFilters() | ||||||
|  |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { DocumentService } from './rest/document.service'; | |||||||
|  |  | ||||||
| /** | /** | ||||||
|  * This service manages the document list which is displayed using the document list view. |  * 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 |  * This service also serves saved views by transparently switching between the document list | ||||||
|  * and saved views on request. See below. |  * and saved views on request. See below. | ||||||
|  */ |  */ | ||||||
| @@ -25,7 +25,7 @@ export class DocumentListViewService { | |||||||
|   currentPage = 1 |   currentPage = 1 | ||||||
|   currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT |   currentPageSize: number = +localStorage.getItem(GENERAL_SETTINGS.DOCUMENT_LIST_SIZE) || GENERAL_SETTINGS.DOCUMENT_LIST_SIZE_DEFAULT | ||||||
|   collectionSize: number |   collectionSize: number | ||||||
|    |  | ||||||
|   /** |   /** | ||||||
|    * This is the current config for the document list. The service will always remember the last settings used for the document list. |    * This is the current config for the document list. The service will always remember the last settings used for the document list. | ||||||
|    */ |    */ | ||||||
| @@ -192,7 +192,7 @@ export class DocumentListViewService { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   constructor(private documentService: DocumentService) {  |   constructor(private documentService: DocumentService) { | ||||||
|     let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) |     let documentListViewConfigJson = sessionStorage.getItem(DOCUMENT_LIST_SERVICE.CURRENT_VIEW_CONFIG) | ||||||
|     if (documentListViewConfigJson) { |     if (documentListViewConfigJson) { | ||||||
|       try { |       try { | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								src-ui/src/app/services/filter-editor-view.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src-ui/src/app/services/filter-editor-view.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import { TestBed } from '@angular/core/testing'; | ||||||
|  |  | ||||||
|  | import { FilterEditorViewService } from './filter-editor-view.service'; | ||||||
|  |  | ||||||
|  | describe('FilterEditorViewService', () => { | ||||||
|  |   let service: FilterEditorViewService; | ||||||
|  |  | ||||||
|  |   beforeEach(() => { | ||||||
|  |     TestBed.configureTestingModule({}); | ||||||
|  |     service = TestBed.inject(FilterEditorViewService); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('should be created', () => { | ||||||
|  |     expect(service).toBeTruthy(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										188
									
								
								src-ui/src/app/services/filter-editor-view.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										188
									
								
								src-ui/src/app/services/filter-editor-view.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,188 @@ | |||||||
|  | import { Injectable } from '@angular/core'; | ||||||
|  | import { Observable } from 'rxjs'; | ||||||
|  | import { TagService } from 'src/app/services/rest/tag.service'; | ||||||
|  | import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||||
|  | import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||||
|  | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
|  | import { FilterRule } from 'src/app/data/filter-rule'; | ||||||
|  | import { FilterRuleType, FILTER_RULE_TYPES, FILTER_CORRESPONDENT, FILTER_DOCUMENT_TYPE, FILTER_HAS_TAG, FILTER_TITLE, FILTER_ADDED_BEFORE, FILTER_ADDED_AFTER, FILTER_CREATED_BEFORE, FILTER_CREATED_AFTER, FILTER_CREATED_YEAR, FILTER_CREATED_MONTH, FILTER_CREATED_DAY } from 'src/app/data/filter-rule-type'; | ||||||
|  | import { Results } from 'src/app/data/results' | ||||||
|  | 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 { NgbDate, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; | ||||||
|  |  | ||||||
|  | @Injectable({ | ||||||
|  |   providedIn: 'root' | ||||||
|  | }) | ||||||
|  | export class FilterEditorViewService { | ||||||
|  |   tags$: Observable<Results<PaperlessTag>> | ||||||
|  |   correspondents$: Observable<Results<PaperlessCorrespondent>> | ||||||
|  |   documentTypes$: Observable<Results<PaperlessDocumentType>> | ||||||
|  |  | ||||||
|  |   tags: PaperlessTag[] = [] | ||||||
|  |   correspondents: PaperlessCorrespondent[] | ||||||
|  |   documentTypes: PaperlessDocumentType[] = [] | ||||||
|  |  | ||||||
|  |   filterRules: FilterRule[] = [] | ||||||
|  |  | ||||||
|  |   constructor(private tagService: TagService, private documentTypeService: DocumentTypeService, private correspondentService: CorrespondentService) { | ||||||
|  |     this.tags$ = this.tagService.listAll() | ||||||
|  |     this.tags$.subscribe(result => this.tags = result.results) | ||||||
|  |     this.correspondents$ = this.correspondentService.listAll() | ||||||
|  |     this.correspondents$.subscribe(result => this.correspondents = result.results) | ||||||
|  |     this.documentTypes$ = this.documentTypeService.listAll() | ||||||
|  |     this.documentTypes$.subscribe(result => this.documentTypes = result.results) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   clear() { | ||||||
|  |     this.filterRules = [] | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   hasFilters() { | ||||||
|  |     return this.filterRules.length > 0 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   set filterText(text: string) { | ||||||
|  |     let filterRules = this.filterRules | ||||||
|  |     let existingRule = filterRules.find(rule => rule.type.id == FILTER_TITLE) | ||||||
|  |     if (existingRule && existingRule.value == text) { | ||||||
|  |       return | ||||||
|  |     } else if (existingRule) { | ||||||
|  |       existingRule.value = text | ||||||
|  |     } else { | ||||||
|  |       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == FILTER_TITLE), value: text}) | ||||||
|  |     } | ||||||
|  |     this.filterRules = filterRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get filterText(): string { | ||||||
|  |     let existingRule = this.filterRules.find(rule => rule.type.id == FILTER_TITLE) | ||||||
|  |     return existingRule ? existingRule.value : '' | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get selectedTags(): PaperlessTag[] { | ||||||
|  |     let tagRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == FILTER_HAS_TAG) | ||||||
|  |     return this.tags?.filter(t => tagRules.find(tr => tr.value == t.id)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get selectedCorrespondents(): PaperlessCorrespondent[] { | ||||||
|  |     let correspondentRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == FILTER_CORRESPONDENT) | ||||||
|  |     return this.correspondents?.filter(c => correspondentRules.find(cr => cr.value == c.id)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get selectedDocumentTypes(): PaperlessDocumentType[] { | ||||||
|  |     let documentTypeRules: FilterRule[] = this.filterRules.filter(fr => fr.type.id == FILTER_DOCUMENT_TYPE) | ||||||
|  |     return this.documentTypes?.filter(dt => documentTypeRules.find(dtr => dtr.value == dt.id)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByTag(tag: PaperlessTag) { | ||||||
|  |     this.toggleFilterByItem(tag, FILTER_HAS_TAG) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByCorrespondent(tag: PaperlessCorrespondent) { | ||||||
|  |     this.toggleFilterByItem(tag, FILTER_CORRESPONDENT) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByDocumentType(tag: PaperlessDocumentType) { | ||||||
|  |     this.toggleFilterByItem(tag, FILTER_DOCUMENT_TYPE) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByTagID(tagID: number) { | ||||||
|  |     this.toggleFitlerByTag(this.tags?.find(t => t.id == tagID)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByCorrespondentID(correspondentID: number) { | ||||||
|  |     this.toggleFitlerByCorrespondent(this.correspondents?.find(t => t.id == correspondentID)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   toggleFitlerByDocumentTypeID(documentTypeID: number) { | ||||||
|  |     this.toggleFitlerByDocumentType(this.documentTypes?.find(t => t.id == documentTypeID)) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private toggleFilterByItem(item: ObjectWithId, filterRuleTypeID: number) { | ||||||
|  |     let filterRules = this.filterRules | ||||||
|  |     let filterRuleType: FilterRuleType = FILTER_RULE_TYPES.find(t => t.id == filterRuleTypeID) | ||||||
|  |     let existingRule = filterRules.find(rule => rule.type.id == filterRuleType.id) | ||||||
|  |  | ||||||
|  |     if (existingRule && existingRule.value == item.id) { | ||||||
|  |       filterRules.splice(filterRules.indexOf(existingRule), 1) | ||||||
|  |     } else if (existingRule && filterRuleType.id == FILTER_HAS_TAG) { | ||||||
|  |       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) | ||||||
|  |     } else if (existingRule && existingRule.value == item.id) { | ||||||
|  |       return | ||||||
|  |     } else if (existingRule) { | ||||||
|  |       existingRule.value = item.id | ||||||
|  |     } else { | ||||||
|  |       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == filterRuleType.id), value: item.id}) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.filterRules = filterRules | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get dateCreatedBefore(): NgbDateStruct { | ||||||
|  |     let createdBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_BEFORE) | ||||||
|  |     return createdBeforeRule ? { | ||||||
|  |       year: createdBeforeRule.value.substring(0,4), | ||||||
|  |       month: createdBeforeRule.value.substring(5,7), | ||||||
|  |       day: createdBeforeRule.value.substring(8,10) | ||||||
|  |     } : undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get dateCreatedAfter(): NgbDateStruct { | ||||||
|  |     let createdAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_CREATED_AFTER) | ||||||
|  |     return createdAfterRule ? { | ||||||
|  |       year: createdAfterRule.value.substring(0,4), | ||||||
|  |       month: createdAfterRule.value.substring(5,7), | ||||||
|  |       day: createdAfterRule.value.substring(8,10) | ||||||
|  |     } : undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get dateAddedBefore(): NgbDateStruct { | ||||||
|  |     let addedBeforeRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_BEFORE) | ||||||
|  |     return addedBeforeRule ? { | ||||||
|  |       year: addedBeforeRule.value.substring(0,4), | ||||||
|  |       month: addedBeforeRule.value.substring(5,7), | ||||||
|  |       day: addedBeforeRule.value.substring(8,10) | ||||||
|  |     } : undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get dateAddedAfter(): NgbDateStruct { | ||||||
|  |     let addedAfterRule: FilterRule = this.filterRules.find(fr => fr.type.id == FILTER_ADDED_AFTER) | ||||||
|  |     return addedAfterRule ? { | ||||||
|  |       year: addedAfterRule.value.substring(0,4), | ||||||
|  |       month: addedAfterRule.value.substring(5,7), | ||||||
|  |       day: addedAfterRule.value.substring(8,10) | ||||||
|  |     } : undefined | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setDateCreatedBefore(date: NgbDateStruct) { | ||||||
|  |     this.setDate(date, FILTER_CREATED_BEFORE) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setDateCreatedAfter(date: NgbDateStruct) { | ||||||
|  |     this.setDate(date, FILTER_CREATED_AFTER) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setDateAddedBefore(date: NgbDateStruct) { | ||||||
|  |     this.setDate(date, FILTER_ADDED_BEFORE) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setDateAddedAfter(date: NgbDateStruct) { | ||||||
|  |     this.setDate(date, FILTER_ADDED_AFTER) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setDate(date: NgbDateStruct, dateRuleTypeID: number) { | ||||||
|  |     let filterRules = this.filterRules | ||||||
|  |     let existingRule = filterRules.find(rule => rule.type.id == dateRuleTypeID) | ||||||
|  |     let newValue = `${date.year}-${date.month.toString().padStart(2,'0')}-${date.day.toString().padStart(2,'0')}` // YYYY-MM-DD | ||||||
|  |  | ||||||
|  |     if (existingRule) { | ||||||
|  |       existingRule.value = newValue | ||||||
|  |     } else { | ||||||
|  |       filterRules.push({type: FILTER_RULE_TYPES.find(t => t.id == dateRuleTypeID), value: newValue}) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.filterRules = filterRules | ||||||
|  |   } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon