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()"> | ||||
|   <div class="selected-icon mr-1"> | ||||
|     <svg *ngIf="selected" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|       <use xlink:href="assets/bootstrap-icons.svg#check" /> | ||||
|     <svg *ngIf="getSelectedIconName() !== ''" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor"> | ||||
|       <use attr.xlink:href="assets/bootstrap-icons.svg#{{getSelectedIconName()}}" /> | ||||
|     </svg> | ||||
|   </div> | ||||
|   <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 { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| import { SelectableItem, SelectableItemState } from '../filterable-dropdown.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-filterable-dropdown-button', | ||||
| @@ -11,10 +12,11 @@ import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'; | ||||
| export class FilterableDropdownButtonComponent implements OnInit { | ||||
|  | ||||
|   @Input() | ||||
|   item: PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent | ||||
|   selectableItem: SelectableItem | ||||
|  | ||||
|   @Input() | ||||
|   selected: boolean | ||||
|   get item(): PaperlessTag | PaperlessDocumentType | PaperlessCorrespondent { | ||||
|     return this.selectableItem?.item | ||||
|   } | ||||
|  | ||||
|   @Output() | ||||
|   toggle = new EventEmitter() | ||||
| @@ -26,7 +28,14 @@ export class FilterableDropdownButtonComponent implements OnInit { | ||||
|   } | ||||
|  | ||||
|   toggleItem(): void { | ||||
|     this.selected = !this.selected | ||||
|     this.toggle.emit(this.item) | ||||
|     this.selectableItem.state = (this.selectableItem.state == SelectableItemState.NotSelected || this.selectableItem.state == SelectableItemState.PartiallySelected) ? SelectableItemState.Selected : SelectableItemState.NotSelected | ||||
|     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> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div *ngIf="items" class="items"> | ||||
|         <ng-container *ngFor="let item of items | filter: filterText; let i = index"> | ||||
|           <app-filterable-dropdown-button [item]="item" [selected]="isItemSelected(item)" (toggle)="toggleItem($event)"></app-filterable-dropdown-button> | ||||
|       <div *ngIf="selectableItems" class="items"> | ||||
|         <ng-container *ngFor="let selectableItem of selectableItems | filter: filterText"> | ||||
|           <app-filterable-dropdown-button [selectableItem]="selectableItem" (toggle)="toggleItem($event)"></app-filterable-dropdown-button> | ||||
|         </ng-container> | ||||
|       </div> | ||||
|     </div> | ||||
|   | ||||
| @@ -1,8 +1,22 @@ | ||||
| import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core'; | ||||
| 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 { 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({ | ||||
|   selector: 'app-filterable-dropdown', | ||||
|   templateUrl: './filterable-dropdown.component.html', | ||||
| @@ -13,10 +27,26 @@ export class FilterableDropdownComponent { | ||||
|   constructor(private filterPipe: FilterPipe) { } | ||||
|  | ||||
|   @Input() | ||||
|   items: ObjectWithId[] | ||||
|   set items(items: ObjectWithId[]) { | ||||
|     if (items) { | ||||
|       this.selectableItems = items.map(i => { | ||||
|         return {item: i, state: SelectableItemState.NotSelected} | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   selectableItems: SelectableItem[] = [] | ||||
|  | ||||
|   @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() | ||||
|   title: string | ||||
| @@ -35,12 +65,8 @@ export class FilterableDropdownComponent { | ||||
|  | ||||
|   filterText: string | ||||
|  | ||||
|   toggleItem(item: ObjectWithId): void { | ||||
|     this.toggle.emit(item) | ||||
|   } | ||||
|  | ||||
|   isItemSelected(item: ObjectWithId): boolean { | ||||
|     return this.itemsSelected?.find(i => i.id == item.id) !== undefined | ||||
|   toggleItem(selectableItem: SelectableItem): void { | ||||
|     this.toggle.emit(selectableItem.item) | ||||
|   } | ||||
|  | ||||
|   dropdownOpenChange(open: boolean): void { | ||||
| @@ -50,12 +76,12 @@ export class FilterableDropdownComponent { | ||||
|       }, 0); | ||||
|     } else { | ||||
|       this.filterText = '' | ||||
|       this.close.emit(this.itemsSelected) | ||||
|       this.close.next() | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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()) | ||||
|     this.dropdown.close() | ||||
|   } | ||||
|   | ||||
| @@ -1,16 +1,17 @@ | ||||
| import { Pipe, PipeTransform } from '@angular/core'; | ||||
| import { SelectableItem } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component'; | ||||
|  | ||||
| @Pipe({ | ||||
|   name: 'filter' | ||||
| }) | ||||
| export class FilterPipe implements PipeTransform { | ||||
|   transform(items: any[], searchText: string): any[] { | ||||
|     if (!items) return []; | ||||
|     if (!searchText) return items; | ||||
|   transform(selectableItems: SelectableItem[], searchText: string): any[] { | ||||
|     if (!selectableItems) return []; | ||||
|     if (!searchText) return selectableItems; | ||||
|  | ||||
|     return items.filter(item => { | ||||
|       return Object.keys(item).some(key => { | ||||
|         return String(item[key]).toLowerCase().includes(searchText.toLowerCase()); | ||||
|     return selectableItems.filter(selectableItem => { | ||||
|       return Object.keys(selectableItem.item).some(key => { | ||||
|         return String(selectableItem.item[key]).toLowerCase().includes(searchText.toLowerCase()); | ||||
|       }); | ||||
|     }); | ||||
|    } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon