mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Basic tags, correspondents & document type dropdowns
This commit is contained in:
		@@ -48,6 +48,7 @@ import { WidgetFrameComponent } from './components/dashboard/widgets/widget-fram
 | 
				
			|||||||
import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
 | 
					import { WelcomeWidgetComponent } from './components/dashboard/widgets/welcome-widget/welcome-widget.component';
 | 
				
			||||||
import { YesNoPipe } from './pipes/yes-no.pipe';
 | 
					import { YesNoPipe } from './pipes/yes-no.pipe';
 | 
				
			||||||
import { FileSizePipe } from './pipes/file-size.pipe';
 | 
					import { FileSizePipe } from './pipes/file-size.pipe';
 | 
				
			||||||
 | 
					import { FilterPipe } from './pipes/filter.pipe';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
  declarations: [
 | 
					  declarations: [
 | 
				
			||||||
@@ -88,7 +89,8 @@ import { FileSizePipe } from './pipes/file-size.pipe';
 | 
				
			|||||||
    WidgetFrameComponent,
 | 
					    WidgetFrameComponent,
 | 
				
			||||||
    WelcomeWidgetComponent,
 | 
					    WelcomeWidgetComponent,
 | 
				
			||||||
    YesNoPipe,
 | 
					    YesNoPipe,
 | 
				
			||||||
    FileSizePipe
 | 
					    FileSizePipe,
 | 
				
			||||||
 | 
					    FilterPipe
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    BrowserModule,
 | 
					    BrowserModule,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
<app-page-header [title]="getTitle()">
 | 
					<app-page-header [title]="getTitle()">
 | 
				
			||||||
 | 
					 | 
				
			||||||
  <div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode"
 | 
					  <div class="btn-group btn-group-toggle" ngbRadioGroup [(ngModel)]="displayMode"
 | 
				
			||||||
    (ngModelChange)="saveDisplayMode()">
 | 
					    (ngModelChange)="saveDisplayMode()">
 | 
				
			||||||
    <label ngbButtonLabel class="btn-outline-primary btn-sm">
 | 
					    <label ngbButtonLabel class="btn-outline-primary btn-sm">
 | 
				
			||||||
@@ -21,6 +20,7 @@
 | 
				
			|||||||
      </svg>
 | 
					      </svg>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </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.sortDirection">
 | 
				
			||||||
    <div ngbDropdown class="btn-group">
 | 
					    <div ngbDropdown class="btn-group">
 | 
				
			||||||
      <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
 | 
					      <button class="btn btn-outline-primary btn-sm" id="dropdownBasic1" ngbDropdownToggle>Sort by</button>
 | 
				
			||||||
@@ -42,13 +42,14 @@
 | 
				
			|||||||
      </svg>
 | 
					      </svg>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="btn-group ml-2">
 | 
					  <div class="btn-group ml-2">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
 | 
					    <button type="button" class="btn btn-sm" [ngClass]="isFiltered ? 'btn-primary' : 'btn-outline-primary'" (click)="showFilter=!showFilter">
 | 
				
			||||||
      <svg class="toolbaricon" fill="currentColor">
 | 
					      <svg class="toolbaricon" fill="currentColor">
 | 
				
			||||||
        <use xlink:href="assets/bootstrap-icons.svg#funnel" />
 | 
					        <use xlink:href="assets/bootstrap-icons.svg#funnel" />
 | 
				
			||||||
      </svg>
 | 
					      </svg>
 | 
				
			||||||
      Filter
 | 
					      Advanced Filters
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="btn-group" ngbDropdown role="group">
 | 
					    <div class="btn-group" ngbDropdown role="group">
 | 
				
			||||||
@@ -58,18 +59,69 @@
 | 
				
			|||||||
          <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
 | 
					          <button ngbDropdownItem *ngFor="let config of savedViewConfigService.getConfigs()" (click)="loadViewConfig(config)">{{config.title}}</button>
 | 
				
			||||||
          <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
 | 
					          <div class="dropdown-divider" *ngIf="savedViewConfigService.getConfigs().length > 0"></div>
 | 
				
			||||||
        </ng-container>
 | 
					        </ng-container>
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
 | 
					        <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.savedViewId">Save "{{list.savedViewTitle}}"</button>
 | 
				
			||||||
        <button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
 | 
					        <button ngbDropdownItem (click)="saveViewConfigAs()">Save as...</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</app-page-header>
 | 
					</app-page-header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="row pb-1 mb-3 align-items-right" >
 | 
				
			||||||
 | 
					  <div class="btn-toolbar col-auto">
 | 
				
			||||||
 | 
					    <div class="btn-group ml-2" ngbDropdown role="group">
 | 
				
			||||||
 | 
					      <button class="btn btn-outline-primary btn-sm" id="dropdownTags" ngbDropdownToggle>Tags</button>
 | 
				
			||||||
 | 
					      <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownTags">
 | 
				
			||||||
 | 
					        <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					          <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags">
 | 
				
			||||||
 | 
					          <ng-container *ngIf="(tags | filter: searchText).length > 0">
 | 
				
			||||||
 | 
					            <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let tag of tags | filter: searchText; let i = index" (click)="filterByTag(tag.id, true)">
 | 
				
			||||||
 | 
					              {{tag.name}}
 | 
				
			||||||
 | 
					              <span class="badge bg-primary text-light rounded-pill">{{tag.document_count}}</span>
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </ng-container>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="btn-group ml-2" ngbDropdown role="group">
 | 
				
			||||||
 | 
					      <button class="btn btn-outline-primary btn-sm" id="dropdownCorrespondents" ngbDropdownToggle>Correspondents</button>
 | 
				
			||||||
 | 
					      <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownCorrespondents">
 | 
				
			||||||
 | 
					        <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					          <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter correspondents">
 | 
				
			||||||
 | 
					          <ng-container *ngIf="(correspondents | filter: searchText).length > 0">
 | 
				
			||||||
 | 
					            <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let correspondent of correspondents | filter: searchText; let i = index" (click)="filterByCorrespondent(correspondent.id, true)">
 | 
				
			||||||
 | 
					              {{correspondent.name}}
 | 
				
			||||||
 | 
					              <span class="badge bg-primary text-light rounded-pill">{{correspondent.document_count}}</span>
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </ng-container>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="btn-group ml-2" ngbDropdown role="group">
 | 
				
			||||||
 | 
					      <button class="btn btn-outline-primary btn-sm" id="dropdownDocumentTypes" ngbDropdownToggle>Document Types</button>
 | 
				
			||||||
 | 
					      <div class="dropdown-menu" ngbDropdownMenu aria-labelledby="dropdownDocumentTypes">
 | 
				
			||||||
 | 
					        <div class="list-group list-group-flush">
 | 
				
			||||||
 | 
					          <input class="list-group-item form-control" type="text" [(ngModel)]="searchText" placeholder="Filter tags">
 | 
				
			||||||
 | 
					          <ng-container *ngIf="(documentTypes | filter: searchText).length > 0">
 | 
				
			||||||
 | 
					            <button class="list-group-item list-group-item-action d-flex justify-content-between align-items-center" role="menuitem" *ngFor="let documentType of documentTypes | filter: searchText; let i = index" (click)="filterByDocumentType(documentType.id, true)">
 | 
				
			||||||
 | 
					              {{documentType.name}}
 | 
				
			||||||
 | 
					              <span class="badge bg-primary text-light rounded-pill">{{documentType.document_count}}</span>
 | 
				
			||||||
 | 
					            </button>
 | 
				
			||||||
 | 
					          </ng-container>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="card w-100 mb-3" [hidden]="!showFilter">
 | 
					<div class="card w-100 mb-3" [hidden]="!showFilter">
 | 
				
			||||||
  <div class="card-body">
 | 
					  <div class="card-body">
 | 
				
			||||||
    <h5 class="card-title">Filter</h5>
 | 
					    <h5 class="card-title">Advanced Filters</h5>
 | 
				
			||||||
    <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor>
 | 
					    <app-filter-editor [(filterRules)]="filterRules" (apply)="applyFilterRules()" (clear)="clearFilterRules()"></app-filter-editor>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
@@ -125,5 +177,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
 | 
					<div class=" m-n2 row" *ngIf="displayMode == 'smallCards'">
 | 
				
			||||||
  <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>    
 | 
					  <app-document-card-small [document]="d" *ngFor="let d of list.documents" (clickTag)="filterByTag($event)" (clickCorrespondent)="filterByCorrespondent($event)"></app-document-card-small>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script type="text/ng-template" id="customTemplate.html">
 | 
				
			||||||
 | 
					  <a>
 | 
				
			||||||
 | 
					      <img ng-src="http://upload.wikimedia.org/wikipedia/commons/thumb/{{match.model.flag}}" width="16">
 | 
				
			||||||
 | 
					      <span ng-bind-html="match.label | uibTypeaheadHighlight:query"></span>
 | 
				
			||||||
 | 
					  </a>
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,12 @@ import { SavedViewConfigService } from 'src/app/services/saved-view-config.servi
 | 
				
			|||||||
import { Toast, ToastService } from 'src/app/services/toast.service';
 | 
					import { Toast, ToastService } from 'src/app/services/toast.service';
 | 
				
			||||||
import { environment } from 'src/environments/environment';
 | 
					import { environment } from 'src/environments/environment';
 | 
				
			||||||
import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
 | 
					import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component';
 | 
				
			||||||
 | 
					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 { 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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
  selector: 'app-document-list',
 | 
					  selector: 'app-document-list',
 | 
				
			||||||
@@ -25,13 +31,20 @@ export class DocumentListComponent implements OnInit {
 | 
				
			|||||||
    public route: ActivatedRoute,
 | 
					    public route: ActivatedRoute,
 | 
				
			||||||
    private toastService: ToastService,
 | 
					    private toastService: ToastService,
 | 
				
			||||||
    public modalService: NgbModal,
 | 
					    public modalService: NgbModal,
 | 
				
			||||||
    private titleService: Title) { }
 | 
					    private titleService: Title,
 | 
				
			||||||
 | 
					    private tagService: TagService,
 | 
				
			||||||
 | 
					    private correspondentService: CorrespondentService,
 | 
				
			||||||
 | 
					    private documentTypeService: DocumentTypeService) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  displayMode = 'smallCards' // largeCards, smallCards, details
 | 
					  displayMode = 'smallCards' // largeCards, smallCards, details
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  filterRules: FilterRule[] = []
 | 
					  filterRules: FilterRule[] = []
 | 
				
			||||||
  showFilter = false
 | 
					  showFilter = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  tags: PaperlessTag[] = []
 | 
				
			||||||
 | 
					  correspondents: PaperlessCorrespondent[] = []
 | 
				
			||||||
 | 
					  documentTypes: PaperlessDocumentType[] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  get isFiltered() {
 | 
					  get isFiltered() {
 | 
				
			||||||
    return this.list.filterRules?.length > 0
 | 
					    return this.list.filterRules?.length > 0
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -67,6 +80,9 @@ export class DocumentListComponent implements OnInit {
 | 
				
			|||||||
      this.list.clear()
 | 
					      this.list.clear()
 | 
				
			||||||
      this.list.reload()
 | 
					      this.list.reload()
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					    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)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  applyFilterRules() {
 | 
					  applyFilterRules() {
 | 
				
			||||||
@@ -103,8 +119,8 @@ export class DocumentListComponent implements OnInit {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  filterByTag(tag_id: number) {
 | 
					  filterByTag(tag_id: number, singleton: boolean = false) {
 | 
				
			||||||
    let filterRules = this.list.filterRules
 | 
					    let filterRules = singleton ? [] : this.list.filterRules
 | 
				
			||||||
    if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) {
 | 
					    if (filterRules.find(rule => rule.type.id == FILTER_HAS_TAG && rule.value == tag_id)) {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -114,8 +130,8 @@ export class DocumentListComponent implements OnInit {
 | 
				
			|||||||
    this.applyFilterRules()
 | 
					    this.applyFilterRules()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  filterByCorrespondent(correspondent_id: number) {
 | 
					  filterByCorrespondent(correspondent_id: number, singleton: boolean = false) {
 | 
				
			||||||
    let filterRules = this.list.filterRules
 | 
					    let filterRules = singleton ? [] : this.list.filterRules
 | 
				
			||||||
    let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT)
 | 
					    let existing_rule = filterRules.find(rule => rule.type.id == FILTER_CORRESPONDENT)
 | 
				
			||||||
    if (existing_rule && existing_rule.value == correspondent_id) {
 | 
					    if (existing_rule && existing_rule.value == correspondent_id) {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
@@ -128,8 +144,8 @@ export class DocumentListComponent implements OnInit {
 | 
				
			|||||||
    this.applyFilterRules()
 | 
					    this.applyFilterRules()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  filterByDocumentType(document_type_id: number) {
 | 
					  filterByDocumentType(document_type_id: number, singleton: boolean = false) {
 | 
				
			||||||
    let filterRules = this.list.filterRules
 | 
					    let filterRules = singleton ? [] : this.list.filterRules
 | 
				
			||||||
    let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE)
 | 
					    let existing_rule = filterRules.find(rule => rule.type.id == FILTER_DOCUMENT_TYPE)
 | 
				
			||||||
    if (existing_rule && existing_rule.value == document_type_id) {
 | 
					    if (existing_rule && existing_rule.value == document_type_id) {
 | 
				
			||||||
      return
 | 
					      return
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					   }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user