mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06: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