mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Refactor filterable dropdowns to allow intermediate state
This commit is contained in:
		| @@ -1,7 +1,7 @@ | |||||||
| <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()"> | <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"> |   <div class="selected-icon mr-1"> | ||||||
|     <svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> |     <svg *ngIf="getSelectedIconName() !== ''" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||||
|       <use xlink:href="assets/bootstrap-icons.svg#check" /> |       <use attr.xlink:href="assets/bootstrap-icons.svg#{{getSelectedIconName()}}" /> | ||||||
|     </svg> |     </svg> | ||||||
|   </div> |   </div> | ||||||
|   <div class="mr-1"> |   <div class="mr-1"> | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core'; | |||||||
| 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 { SelectableItem, SelectableItemState } from '../filterable-dropdown.component'; | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-filterable-dropdown-button', |   selector: 'app-filterable-dropdown-button', | ||||||
| @@ -11,10 +12,11 @@ import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | |||||||
| export class FilterableDropdownButtonComponent implements OnInit { | export class FilterableDropdownButtonComponent implements OnInit { | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent |   selectableItem: SelectableItem | ||||||
|  |  | ||||||
|   @Input() |   get item(): PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent { | ||||||
|   selected: boolean |     return this.selectableItem?.item | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   toggle = new EventEmitter() |   toggle = new EventEmitter() | ||||||
| @@ -26,7 +28,14 @@ export class FilterableDropdownButtonComponent implements OnInit { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   toggleItem(): void { |   toggleItem(): void { | ||||||
|     this.selected = !this.selected |     this.selectableItem.state = (this.selectableItem.state == SelectableItemState.NotSelected || this.selectableItem.state == SelectableItemState.PartiallySelected) ? SelectableItemState.Selected : SelectableItemState.NotSelected | ||||||
|     this.toggle.emit(this.item) |     this.toggle.emit(this.selectableItem) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   getSelectedIconName() { | ||||||
|  |     let iconName = '' | ||||||
|  |     if (this.selectableItem?.state == SelectableItemState.Selected) iconName = 'check' | ||||||
|  |     else if (this.selectableItem?.state == SelectableItemState.PartiallySelected) iconName = 'minus' | ||||||
|  |     return iconName | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -19,9 +19,9 @@ | |||||||
|           <input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> |           <input class="form-control" type="text" [(ngModel)]="filterText" placeholder="Filter {{title}}" (keyup.enter)="listFilterEnter()" #listFilterTextInput> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <div *ngIf="items" class="items"> |       <div *ngIf="selectableItems" class="items"> | ||||||
|         <ng-container *ngFor="let item of items | filter: filterText; let i = index"> |         <ng-container *ngFor="let selectableItem of selectableItems | filter: filterText"> | ||||||
|           <app-filterable-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filterable-dropdown-button> |           <app-filterable-dropdown-button [selectableItem]="selectableItem" (toggle)="toggleItem($event)"></app-filterable-dropdown-button> | ||||||
|         </ng-container> |         </ng-container> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   | |||||||
| @@ -1,8 +1,22 @@ | |||||||
| import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; | import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
|  | 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 { FilterPipe } from  'src/app/pipes/filter.pipe'; | import { FilterPipe } from  'src/app/pipes/filter.pipe'; | ||||||
| import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | ||||||
|  |  | ||||||
|  | export interface SelectableItem { | ||||||
|  |   item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent, | ||||||
|  |   state: SelectableItemState | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export enum SelectableItemState { | ||||||
|  |   NotSelected = 0, | ||||||
|  |   Selected = 1, | ||||||
|  |   PartiallySelected = 2 | ||||||
|  | } | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-filterable-dropdown', |   selector: 'app-filterable-dropdown', | ||||||
|   templateUrl: './filterable-dropdown.component.html', |   templateUrl: './filterable-dropdown.component.html', | ||||||
| @@ -13,10 +27,26 @@ export class FilterableDropdownComponent { | |||||||
|   constructor(private filterPipe: FilterPipe) { } |   constructor(private filterPipe: FilterPipe) { } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   items: ObjectWithId[] |   set items(items: ObjectWithId[]) { | ||||||
|  |     if (items) { | ||||||
|  |       this.selectableItems = items.map(i => { | ||||||
|  |         return {item: i, state: SelectableItemState.NotSelected} | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   selectableItems: SelectableItem[] = [] | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   itemsSelected: ObjectWithId[] |   set itemsSelected(itemsSelected: ObjectWithId[]) { | ||||||
|  |     this.selectableItems.forEach(i => { | ||||||
|  |       i.state = (itemsSelected.find(is => is.id == i.item.id)) ? SelectableItemState.Selected : SelectableItemState.NotSelected | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get itemsSelected() :ObjectWithId[] { | ||||||
|  |     return this.selectableItems.filter(si => si.state == SelectableItemState.Selected).map(si => si.item) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   title: string |   title: string | ||||||
| @@ -35,12 +65,8 @@ export class FilterableDropdownComponent { | |||||||
|  |  | ||||||
|   filterText: string |   filterText: string | ||||||
|  |  | ||||||
|   toggleItem(item: ObjectWithId): void { |   toggleItem(selectableItem: SelectableItem): void { | ||||||
|     this.toggle.emit(item) |     this.toggle.emit(selectableItem.item) | ||||||
|   } |  | ||||||
|  |  | ||||||
|   isItemSelected(item: ObjectWithId): boolean { |  | ||||||
|     return this.itemsSelected?.find(i => i.id == item.id) !== undefined |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   dropdownOpenChange(open: boolean): void { |   dropdownOpenChange(open: boolean): void { | ||||||
| @@ -50,12 +76,12 @@ export class FilterableDropdownComponent { | |||||||
|       }, 0); |       }, 0); | ||||||
|     } else { |     } else { | ||||||
|       this.filterText = '' |       this.filterText = '' | ||||||
|       this.close.emit(this.itemsSelected) |       this.close.next() | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   listFilterEnter(): void { |   listFilterEnter(): void { | ||||||
|     let filtered = this.filterPipe.transform(this.items, this.filterText) |     let filtered = this.filterPipe.transform(this.selectableItems, this.filterText) | ||||||
|     if (filtered.length == 1) this.toggleItem(filtered.shift()) |     if (filtered.length == 1) this.toggleItem(filtered.shift()) | ||||||
|     this.dropdown.close() |     this.dropdown.close() | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,16 +1,17 @@ | |||||||
| import { Pipe, PipeTransform } from '@angular/core'; | import { Pipe, PipeTransform } from '@angular/core'; | ||||||
|  | import { SelectableItem } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component'; | ||||||
|  |  | ||||||
| @Pipe({ | @Pipe({ | ||||||
|   name: 'filter' |   name: 'filter' | ||||||
| }) | }) | ||||||
| export class FilterPipe implements PipeTransform { | export class FilterPipe implements PipeTransform { | ||||||
|   transform(items: any[], searchText: string): any[] { |   transform(selectableItems: SelectableItem[], searchText: string): any[] { | ||||||
|     if (!items) return []; |     if (!selectableItems) return []; | ||||||
|     if (!searchText) return items; |     if (!searchText) return selectableItems; | ||||||
|  |  | ||||||
|     return items.filter(item => { |     return selectableItems.filter(selectableItem => { | ||||||
|       return Object.keys(item).some(key => { |       return Object.keys(selectableItem.item).some(key => { | ||||||
|         return String(item[key]).toLowerCase().includes(searchText.toLowerCase()); |         return String(selectableItem.item[key]).toLowerCase().includes(searchText.toLowerCase()); | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|    } |    } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon