mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 19:17:13 -05:00 
			
		
		
		
	rework the bulk editor
This commit is contained in:
		
							parent
							
								
									67953c98a9
								
							
						
					
					
						commit
						8af0259671
					
				@ -1,12 +1,12 @@
 | 
			
		||||
<div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown">
 | 
			
		||||
  <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="type !== types.Editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
 | 
			
		||||
  <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="!editing && selectionModel.selectionSize() > 0 ? 'btn-primary' : 'btn-outline-primary'">
 | 
			
		||||
    <div class="d-none d-md-inline">{{title}}</div>
 | 
			
		||||
    <div class="d-inline-block d-md-none">
 | 
			
		||||
      <svg class="toolbaricon" fill="currentColor">
 | 
			
		||||
        <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" />
 | 
			
		||||
      </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
    <ng-container *ngIf="type !== types.Editing && selectionModel.selectionSize() > 0">
 | 
			
		||||
    <ng-container *ngIf="!editing && selectionModel.selectionSize() > 0">
 | 
			
		||||
      <div class="badge bg-secondary text-light rounded-pill badge-corner">
 | 
			
		||||
        {{selectionModel.selectionSize()}}
 | 
			
		||||
      </div>
 | 
			
		||||
@ -24,8 +24,8 @@
 | 
			
		||||
          <app-toggleable-dropdown-button [item]="item" [state]="selectionModel.get(item.id)" (toggle)="selectionModel.toggle(item.id)"></app-toggleable-dropdown-button>
 | 
			
		||||
        </ng-container>
 | 
			
		||||
      </div>
 | 
			
		||||
      <button *ngIf="type == types.Editing" class="list-group-item list-group-item-action bg-light" (click)="dropdown.close()" [disabled]="!hasBeenToggled || (toggleableItems | filter: filterText).length == 0">
 | 
			
		||||
        <small class="ml-1" [ngClass]="{'font-weight-bold': hasBeenToggled && (toggleableItems | filter: filterText).length > 0}">Apply</small>
 | 
			
		||||
      <button *ngIf="editing" class="list-group-item list-group-item-action bg-light" (click)="applyClicked()" [disabled]="!selectionModel.isDirty()">
 | 
			
		||||
        <small class="ml-1" [ngClass]="{'font-weight-bold': selectionModel.isDirty()}">Apply</small>
 | 
			
		||||
        <svg width="1.5em" height="1em" viewBox="0 0 16 16" fill="currentColor">
 | 
			
		||||
          <use xlink:href="assets/bootstrap-icons.svg#arrow-right" />
 | 
			
		||||
        </svg>
 | 
			
		||||
 | 
			
		||||
@ -1,14 +1,13 @@
 | 
			
		||||
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
 | 
			
		||||
import { FilterPipe } from  'src/app/pipes/filter.pipe';
 | 
			
		||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
 | 
			
		||||
import { ToggleableItem, ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
			
		||||
import { ToggleableItemState } from './toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
			
		||||
import { MatchingModel } from 'src/app/data/matching-model';
 | 
			
		||||
import { Subject } from 'rxjs';
 | 
			
		||||
import { ThrowStmt } from '@angular/compiler';
 | 
			
		||||
 | 
			
		||||
export enum FilterableDropdownType {
 | 
			
		||||
  Filtering = 'filtering',
 | 
			
		||||
  Editing = 'editing'
 | 
			
		||||
export interface ChangedItems {
 | 
			
		||||
  itemsToAdd: MatchingModel[],
 | 
			
		||||
  itemsToRemove: MatchingModel[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class FilterableDropdownSelectionModel {
 | 
			
		||||
@ -19,31 +18,37 @@ export class FilterableDropdownSelectionModel {
 | 
			
		||||
 | 
			
		||||
  items: MatchingModel[] = []
 | 
			
		||||
 | 
			
		||||
  selection = new Map<number, ToggleableItemState>()
 | 
			
		||||
  private selectionStates = new Map<number, ToggleableItemState>()
 | 
			
		||||
 | 
			
		||||
  private temporarySelectionStates = new Map<number, ToggleableItemState>()
 | 
			
		||||
 | 
			
		||||
  getSelectedItems() {
 | 
			
		||||
    return this.items.filter(i => this.selection.get(i.id) == ToggleableItemState.Selected)
 | 
			
		||||
    return this.items.filter(i => this.temporarySelectionStates.get(i.id) == ToggleableItemState.Selected)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set(id: number, state: ToggleableItemState, fireEvent = true) {
 | 
			
		||||
    this.selection.set(id, state)
 | 
			
		||||
    if (state == ToggleableItemState.NotSelected) {
 | 
			
		||||
      this.temporarySelectionStates.delete(id)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.temporarySelectionStates.set(id, state)
 | 
			
		||||
    }
 | 
			
		||||
    if (fireEvent) {
 | 
			
		||||
      this.changed.next(this)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggle(id: number, fireEvent = true) {
 | 
			
		||||
    let state = this.selection.get(id)
 | 
			
		||||
    let state = this.temporarySelectionStates.get(id)
 | 
			
		||||
    if (state == null || state != ToggleableItemState.Selected) {
 | 
			
		||||
      this.selection.set(id, ToggleableItemState.Selected)
 | 
			
		||||
      this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
 | 
			
		||||
    } else if (state == ToggleableItemState.Selected) {
 | 
			
		||||
      this.selection.set(id, ToggleableItemState.NotSelected)
 | 
			
		||||
      this.temporarySelectionStates.delete(id)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!this.multiple) {
 | 
			
		||||
      for (let key of this.selection.keys()) {
 | 
			
		||||
      for (let key of this.temporarySelectionStates.keys()) {
 | 
			
		||||
        if (key != id) {
 | 
			
		||||
          this.selection.set(key, ToggleableItemState.NotSelected)
 | 
			
		||||
          this.temporarySelectionStates.delete(key)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@ -55,7 +60,7 @@ export class FilterableDropdownSelectionModel {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get(id: number) {
 | 
			
		||||
    return this.selection.get(id) || ToggleableItemState.NotSelected
 | 
			
		||||
    return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectionSize() {
 | 
			
		||||
@ -63,11 +68,47 @@ export class FilterableDropdownSelectionModel {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  clear(fireEvent = true) {
 | 
			
		||||
    this.selection.clear()
 | 
			
		||||
    this.temporarySelectionStates.clear()
 | 
			
		||||
    if (fireEvent) {
 | 
			
		||||
      this.changed.next(this)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isDirty() {
 | 
			
		||||
    if (!Array.from(this.temporarySelectionStates.keys()).every(id => this.temporarySelectionStates.get(id) == this.selectionStates.get(id))) {
 | 
			
		||||
      return true
 | 
			
		||||
    } else if (!Array.from(this.selectionStates.keys()).every(id => this.selectionStates.get(id) == this.temporarySelectionStates.get(id))) {
 | 
			
		||||
      return true
 | 
			
		||||
    } else {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init(map) {
 | 
			
		||||
    this.temporarySelectionStates = map
 | 
			
		||||
    this.apply()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  apply() {
 | 
			
		||||
    this.selectionStates.clear()
 | 
			
		||||
    this.temporarySelectionStates.forEach((value, key) => {
 | 
			
		||||
      this.selectionStates.set(key, value)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reset() {
 | 
			
		||||
    this.temporarySelectionStates.clear()
 | 
			
		||||
    this.selectionStates.forEach((value, key) => {
 | 
			
		||||
      this.temporarySelectionStates.set(key, value)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  diff(): ChangedItems {
 | 
			
		||||
    return {
 | 
			
		||||
      itemsToAdd: this.items.filter(item => this.temporarySelectionStates.get(item.id) == ToggleableItemState.Selected && this.selectionStates.get(item.id) != ToggleableItemState.Selected),
 | 
			
		||||
      itemsToRemove: this.items.filter(item => !this.temporarySelectionStates.has(item.id) && this.selectionStates.has(item.id)),
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
@ -131,35 +172,35 @@ export class FilterableDropdownComponent {
 | 
			
		||||
  icon: string
 | 
			
		||||
 | 
			
		||||
  @Input()
 | 
			
		||||
  type: FilterableDropdownType = FilterableDropdownType.Filtering
 | 
			
		||||
  editing = false
 | 
			
		||||
 | 
			
		||||
  types = FilterableDropdownType
 | 
			
		||||
  @Output()
 | 
			
		||||
  apply = new EventEmitter<ChangedItems>()
 | 
			
		||||
 | 
			
		||||
  hasBeenToggled:boolean = false
 | 
			
		||||
  @Output()
 | 
			
		||||
  open = new EventEmitter()
 | 
			
		||||
 | 
			
		||||
  constructor(private filterPipe: FilterPipe) {
 | 
			
		||||
    this.selectionModel = new FilterableDropdownSelectionModel()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleItem(toggleableItem: ToggleableItem): void {
 | 
			
		||||
    // if (this.singular && toggleableItem.state == ToggleableItemState.Selected) {
 | 
			
		||||
    //   this.selectionModel.items.filter(ti => ti.item.id !== toggleableItem.item.id).forEach(ti => ti.state = ToggleableItemState.NotSelected)
 | 
			
		||||
    // }
 | 
			
		||||
    // this.hasBeenToggled = true
 | 
			
		||||
    // this.toggle.emit(toggleableItem.item)
 | 
			
		||||
  applyClicked() {
 | 
			
		||||
    if (this.selectionModel.isDirty()) {
 | 
			
		||||
      this.dropdown.close()
 | 
			
		||||
      this.apply.emit(this.selectionModel.diff())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  dropdownOpenChange(open: boolean): void {
 | 
			
		||||
    // if (open) {
 | 
			
		||||
    //   setTimeout(() => {
 | 
			
		||||
    //     this.listFilterTextInput.nativeElement.focus();
 | 
			
		||||
    //   }, 0)
 | 
			
		||||
    //   this.hasBeenToggled = false
 | 
			
		||||
    //   this.open.next()
 | 
			
		||||
    // } else {
 | 
			
		||||
    //   this.filterText = ''
 | 
			
		||||
    //   if (this.type == FilterableDropdownType.Editing) this.editingComplete.emit(this.toggleableItems)
 | 
			
		||||
    // }
 | 
			
		||||
    if (open) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        this.listFilterTextInput.nativeElement.focus();
 | 
			
		||||
      }, 0)
 | 
			
		||||
      this.selectionModel.reset()
 | 
			
		||||
      this.open.next()
 | 
			
		||||
    } else {
 | 
			
		||||
      this.filterText = ''
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listFilterEnter(): void {
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
<!-- <div class="row">
 | 
			
		||||
<div class="row">
 | 
			
		||||
  <div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
 | 
			
		||||
    <button class="btn btn-sm btn-outline-danger" (click)="documentList.selectNone()">
 | 
			
		||||
    <button class="btn btn-sm btn-outline-danger" (click)="list.selectNone()">
 | 
			
		||||
      <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
 | 
			
		||||
        <use xlink:href="assets/bootstrap-icons.svg#slash-circle" />
 | 
			
		||||
      </svg>
 | 
			
		||||
@ -11,13 +11,13 @@
 | 
			
		||||
  <div class="col-auto mb-2 mb-xl-0" role="group" aria-label="Select">
 | 
			
		||||
    <label class="mr-2 mb-0">Select:</label>
 | 
			
		||||
    <div class="btn-group">
 | 
			
		||||
      <button class="btn btn-sm btn-outline-primary" (click)="documentList.selectPage()">
 | 
			
		||||
      <button class="btn btn-sm btn-outline-primary" (click)="list.selectPage()">
 | 
			
		||||
        <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
 | 
			
		||||
          <use xlink:href="assets/bootstrap-icons.svg#file-earmark-check" />
 | 
			
		||||
        </svg>
 | 
			
		||||
        Page
 | 
			
		||||
      </button>
 | 
			
		||||
      <button class="btn btn-sm btn-outline-primary" (click)="documentList.selectAll()">
 | 
			
		||||
      <button class="btn btn-sm btn-outline-primary" (click)="list.selectAll()">
 | 
			
		||||
        <svg width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor">
 | 
			
		||||
          <use xlink:href="assets/bootstrap-icons.svg#check-all" />
 | 
			
		||||
        </svg>
 | 
			
		||||
@ -30,22 +30,26 @@
 | 
			
		||||
    <div class="d-flex">
 | 
			
		||||
      <label class="ml-auto mt-1 mb-0 mr-2">Edit:</label>
 | 
			
		||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill"
 | 
			
		||||
        [toggleableItems]="tagsToggleableItems"
 | 
			
		||||
        [type]="dropdownTypes.Editing"
 | 
			
		||||
        (open)="tagsDropdownOpen()"
 | 
			
		||||
        (editingComplete)="setTags($event)">
 | 
			
		||||
        [items]="tags"
 | 
			
		||||
        [editing]="true"
 | 
			
		||||
        [multiple]="true"
 | 
			
		||||
        (open)="openTagsDropdown()"
 | 
			
		||||
        [(selectionModel)]="tagSelectionModel"
 | 
			
		||||
        (apply)="setTags($event)">
 | 
			
		||||
      </app-filterable-dropdown>
 | 
			
		||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" singular="true"
 | 
			
		||||
        [toggleableItems]="correspondentsToggleableItems"
 | 
			
		||||
        [type]="dropdownTypes.Editing"
 | 
			
		||||
        (open)="correspondentsDropdownOpen()"
 | 
			
		||||
        (editingComplete)="setCorrespondents($event)">
 | 
			
		||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill"
 | 
			
		||||
        [items]="correspondents"
 | 
			
		||||
        [editing]="true"
 | 
			
		||||
        (open)="openCorrespondentDropdown()"
 | 
			
		||||
        [(selectionModel)]="correspondentSelectionModel"
 | 
			
		||||
        (apply)="setCorrespondents($event)">
 | 
			
		||||
      </app-filterable-dropdown>
 | 
			
		||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill" singular="true"
 | 
			
		||||
        [toggleableItems]="documentTypesToggleableItems"
 | 
			
		||||
        [type]="dropdownTypes.Editing"
 | 
			
		||||
        (open)="documentTypesDropdownOpen()"
 | 
			
		||||
        (editingComplete)="setDocumentTypes($event)">
 | 
			
		||||
      <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill"
 | 
			
		||||
        [items]="documentTypes"
 | 
			
		||||
        [editing]="true"
 | 
			
		||||
        (open)="openDocumentTypeDropdown()"
 | 
			
		||||
        [(selectionModel)]="documentTypeSelectionModel"
 | 
			
		||||
        (apply)="setDocumentTypes($event)">
 | 
			
		||||
      </app-filterable-dropdown>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@ -58,4 +62,4 @@
 | 
			
		||||
      Delete
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
</div> -->
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@ -1,26 +1,19 @@
 | 
			
		||||
import { Component, EventEmitter, Input, Output } from '@angular/core';
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
import { tap } from 'rxjs/operators';
 | 
			
		||||
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 { PaperlessDocument } from 'src/app/data/paperless-document';
 | 
			
		||||
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 { DocumentListViewService } from 'src/app/services/document-list-view.service';
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { DocumentService } from 'src/app/services/rest/document.service';
 | 
			
		||||
import { DocumentService, SelectionDataItem } from 'src/app/services/rest/document.service';
 | 
			
		||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
 | 
			
		||||
import { FilterableDropdownType } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component';
 | 
			
		||||
import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
 | 
			
		||||
import { ToggleableItem, ToggleableItemState } from 'src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
			
		||||
 | 
			
		||||
export interface ChangedItems {
 | 
			
		||||
  itemsToAdd: any[],
 | 
			
		||||
  itemsToRemove: any[]
 | 
			
		||||
}
 | 
			
		||||
import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
 | 
			
		||||
import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-bulk-editor',
 | 
			
		||||
@ -33,69 +26,15 @@ export class BulkEditorComponent {
 | 
			
		||||
  correspondents: PaperlessCorrespondent[]
 | 
			
		||||
  documentTypes: PaperlessDocumentType[]
 | 
			
		||||
 | 
			
		||||
  private initialTagsToggleableItems: ToggleableItem[]
 | 
			
		||||
  private initialCorrespondentsToggleableItems: ToggleableItem[]
 | 
			
		||||
  private initialDocumentTypesToggleableItems: ToggleableItem[]
 | 
			
		||||
 | 
			
		||||
  dropdownTypes = FilterableDropdownType
 | 
			
		||||
 | 
			
		||||
  private _tagsToggleableItems: ToggleableItem[]
 | 
			
		||||
  get tagsToggleableItems(): ToggleableItem[] {
 | 
			
		||||
    let tagsToggleableItems = []
 | 
			
		||||
    let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
 | 
			
		||||
 
 | 
			
		||||
    this.tags?.forEach(t => {
 | 
			
		||||
      let selectedDocumentsWithTag: PaperlessDocument[] = selectedDocuments.filter(d => d.tags.includes(t.id))
 | 
			
		||||
      let state = ToggleableItemState.NotSelected
 | 
			
		||||
      if (selectedDocuments.length > 0 && selectedDocumentsWithTag.length == selectedDocuments.length) state = ToggleableItemState.Selected
 | 
			
		||||
      else if (selectedDocumentsWithTag.length > 0 && selectedDocumentsWithTag.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
 | 
			
		||||
      tagsToggleableItems.push({item: t, state: state, count: selectedDocumentsWithTag.length})
 | 
			
		||||
    })
 | 
			
		||||
    this._tagsToggleableItems = tagsToggleableItems
 | 
			
		||||
    return tagsToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _correspondentsToggleableItems: ToggleableItem[]
 | 
			
		||||
  get correspondentsToggleableItems(): ToggleableItem[] {
 | 
			
		||||
    let correspondentsToggleableItems = []
 | 
			
		||||
    let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
 | 
			
		||||
 | 
			
		||||
    this.correspondents?.forEach(c => {
 | 
			
		||||
      let selectedDocumentsWithCorrespondent: PaperlessDocument[] = selectedDocuments.filter(d => d.correspondent == c.id)
 | 
			
		||||
      let state = ToggleableItemState.NotSelected
 | 
			
		||||
      if (selectedDocuments.length > 0 && selectedDocumentsWithCorrespondent.length == selectedDocuments.length) state = ToggleableItemState.Selected
 | 
			
		||||
      else if (selectedDocumentsWithCorrespondent.length > 0 && selectedDocumentsWithCorrespondent.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
 | 
			
		||||
      correspondentsToggleableItems.push({item: c, state: state, count: selectedDocumentsWithCorrespondent.length})
 | 
			
		||||
    })
 | 
			
		||||
    this._correspondentsToggleableItems = correspondentsToggleableItems
 | 
			
		||||
    return correspondentsToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _documentTypesToggleableItems: ToggleableItem[]
 | 
			
		||||
  get documentTypesToggleableItems(): ToggleableItem[] {
 | 
			
		||||
    let documentTypesToggleableItems = []
 | 
			
		||||
    let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id))
 | 
			
		||||
 | 
			
		||||
    this.documentTypes?.forEach(dt => {
 | 
			
		||||
      let selectedDocumentsWithDocumentType: PaperlessDocument[] = selectedDocuments.filter(d => d.document_type == dt.id)
 | 
			
		||||
      let state = ToggleableItemState.NotSelected
 | 
			
		||||
      if (selectedDocuments.length > 0 && selectedDocumentsWithDocumentType.length == selectedDocuments.length) state = ToggleableItemState.Selected
 | 
			
		||||
      else if (selectedDocumentsWithDocumentType.length > 0 && selectedDocumentsWithDocumentType.length < selectedDocuments.length) state = ToggleableItemState.PartiallySelected
 | 
			
		||||
      documentTypesToggleableItems.push({item: dt, state: state, count: selectedDocumentsWithDocumentType.length})
 | 
			
		||||
    })
 | 
			
		||||
    this._documentTypesToggleableItems = documentTypesToggleableItems
 | 
			
		||||
    return documentTypesToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get documentList(): DocumentListViewService {
 | 
			
		||||
    return this.documentListViewService
 | 
			
		||||
  }
 | 
			
		||||
  tagSelectionModel = new FilterableDropdownSelectionModel()
 | 
			
		||||
  correspondentSelectionModel = new FilterableDropdownSelectionModel()
 | 
			
		||||
  documentTypeSelectionModel = new FilterableDropdownSelectionModel()
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private documentTypeService: DocumentTypeService,
 | 
			
		||||
    private tagService: TagService,
 | 
			
		||||
    private correspondentService: CorrespondentService,
 | 
			
		||||
    private documentListViewService: DocumentListViewService,
 | 
			
		||||
    public list: DocumentListViewService,
 | 
			
		||||
    private documentService: DocumentService,
 | 
			
		||||
    private modalService: NgbModal,
 | 
			
		||||
    private openDocumentService: OpenDocumentsService
 | 
			
		||||
@ -107,97 +46,69 @@ export class BulkEditorComponent {
 | 
			
		||||
    this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  tagsDropdownOpen() {
 | 
			
		||||
    this.initialTagsToggleableItems = this._tagsToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  correspondentsDropdownOpen() {
 | 
			
		||||
    this.initialCorrespondentsToggleableItems = this._correspondentsToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  documentTypesDropdownOpen() {
 | 
			
		||||
    this.initialDocumentTypesToggleableItems = this._documentTypesToggleableItems
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private checkForChangedItems(toggleableItemsA: ToggleableItem[], toggleableItemsB: ToggleableItem[]): ChangedItems {
 | 
			
		||||
    let itemsToAdd: any[] = []
 | 
			
		||||
    let itemsToRemove: any[] = []
 | 
			
		||||
    toggleableItemsA.forEach(oldItem => {
 | 
			
		||||
      let newItem = toggleableItemsB.find(nTTI => nTTI.item.id == oldItem.item.id)
 | 
			
		||||
 | 
			
		||||
      if (newItem.state == ToggleableItemState.Selected && (oldItem.state == ToggleableItemState.PartiallySelected || oldItem.state == ToggleableItemState.NotSelected)) itemsToAdd.push(newItem.item)
 | 
			
		||||
      else if (newItem.state == ToggleableItemState.NotSelected && (oldItem.state == ToggleableItemState.Selected || oldItem.state == ToggleableItemState.PartiallySelected)) itemsToRemove.push(newItem.item)
 | 
			
		||||
    })
 | 
			
		||||
    return { itemsToAdd: itemsToAdd, itemsToRemove: itemsToRemove }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private executeBulkOperation(method: string, args): Observable<any> {
 | 
			
		||||
    return this.documentService.bulkEdit(Array.from(this.documentList.selected), method, args).pipe(
 | 
			
		||||
    return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe(
 | 
			
		||||
      tap(() => {
 | 
			
		||||
        this.documentList.reload()
 | 
			
		||||
        this.documentList.selected.forEach(id => {
 | 
			
		||||
        this.list.reload()
 | 
			
		||||
        this.list.selected.forEach(id => {
 | 
			
		||||
          this.openDocumentService.refreshDocument(id)
 | 
			
		||||
        })
 | 
			
		||||
        this.documentList.selectNone()
 | 
			
		||||
        this.list.selectNone()
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setTags(newTagsToggleableItems: ToggleableItem[]) {
 | 
			
		||||
    let changedTags: ChangedItems
 | 
			
		||||
    if (newTagsToggleableItems) {
 | 
			
		||||
      changedTags = this.checkForChangedItems(this.initialTagsToggleableItems, newTagsToggleableItems)
 | 
			
		||||
      if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
 | 
			
		||||
    }
 | 
			
		||||
  private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
 | 
			
		||||
    let selectionData = new Map<number, ToggleableItemState>()
 | 
			
		||||
    items.forEach(i => {
 | 
			
		||||
      if (i.document_count == this.list.selected.size) {
 | 
			
		||||
        selectionData.set(i.id, ToggleableItemState.Selected)
 | 
			
		||||
      } else if (i.document_count > 0) {
 | 
			
		||||
        selectionData.set(i.id, ToggleableItemState.PartiallySelected)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    selectionModel.init(selectionData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
			
		||||
    modal.componentInstance.title = "Confirm Tags Assignment"
 | 
			
		||||
    let action = 'set_tags'
 | 
			
		||||
    let tags
 | 
			
		||||
    let messageFragment = ''
 | 
			
		||||
    let both = changedTags && changedTags.itemsToAdd.length > 0 && changedTags.itemsToRemove.length > 0
 | 
			
		||||
    if (!changedTags) {
 | 
			
		||||
      messageFragment = `remove all tags from`
 | 
			
		||||
    } else {
 | 
			
		||||
      if (changedTags.itemsToAdd.length > 0) {
 | 
			
		||||
        tags = changedTags.itemsToAdd
 | 
			
		||||
        messageFragment = `assign the tag(s) ${changedTags.itemsToAdd.map(t => t.name).join(', ')} to`
 | 
			
		||||
      }
 | 
			
		||||
      if (changedTags.itemsToRemove.length > 0) {
 | 
			
		||||
        if (!both) {
 | 
			
		||||
          action = 'remove_tags'
 | 
			
		||||
          tags = changedTags.itemsToRemove
 | 
			
		||||
        } else {
 | 
			
		||||
          messageFragment += ' and '
 | 
			
		||||
        }
 | 
			
		||||
        messageFragment += `remove the tag(s) ${changedTags.itemsToRemove.map(t => t.name).join(', ')} from`
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
			
		||||
    modal.componentInstance.btnCaption = "Confirm"
 | 
			
		||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
			
		||||
      // TODO: API endpoints for add/remove multiple tags
 | 
			
		||||
      this.executeBulkOperation(action, {"tags": tags ? tags.map(t => t.id) : null}).subscribe(
 | 
			
		||||
        response => {
 | 
			
		||||
          if (!both) modal.close()
 | 
			
		||||
          else {
 | 
			
		||||
            this.executeBulkOperation('remove_tags', {"tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
 | 
			
		||||
              response => {
 | 
			
		||||
                modal.close()
 | 
			
		||||
              })
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
  openTagsDropdown() {
 | 
			
		||||
    this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
 | 
			
		||||
      this.applySelectionData(s.selected_tags, this.tagSelectionModel)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setCorrespondents(newCorrespondentsToggleableItems: ToggleableItem[]) {
 | 
			
		||||
    let changedCorrespondents: ChangedItems
 | 
			
		||||
    if (newCorrespondentsToggleableItems) {
 | 
			
		||||
      changedCorrespondents = this.checkForChangedItems(this.initialCorrespondentsToggleableItems, newCorrespondentsToggleableItems)
 | 
			
		||||
      if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
 | 
			
		||||
    }
 | 
			
		||||
  openDocumentTypeDropdown() {
 | 
			
		||||
    this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
 | 
			
		||||
      this.applySelectionData(s.selected_document_types, this.documentTypeSelectionModel)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openCorrespondentDropdown() {
 | 
			
		||||
    this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
 | 
			
		||||
      this.applySelectionData(s.selected_correspondents, this.correspondentSelectionModel)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setTags(changedTags: ChangedItems) {
 | 
			
		||||
    if (changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 0) return
 | 
			
		||||
 | 
			
		||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
			
		||||
    modal.componentInstance.title = "Confirm Tags Assignment"
 | 
			
		||||
   
 | 
			
		||||
    modal.componentInstance.message = `This operation will modify some tags on all ${this.list.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
			
		||||
    modal.componentInstance.btnCaption = "Confirm"
 | 
			
		||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
			
		||||
      this.executeBulkOperation('modify_tags', {"add_tags": changedTags.itemsToAdd.map(t => t.id), "remove_tags": changedTags.itemsToRemove.map(t => t.id)}).subscribe(
 | 
			
		||||
        response => {
 | 
			
		||||
          this.tagService.clearCache()
 | 
			
		||||
          modal.close()
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setCorrespondents(changedCorrespondents: ChangedItems) {
 | 
			
		||||
    if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
 | 
			
		||||
 | 
			
		||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
			
		||||
    modal.componentInstance.title = "Confirm Correspondent Assignment"
 | 
			
		||||
@ -207,24 +118,21 @@ export class BulkEditorComponent {
 | 
			
		||||
      correspondent = changedCorrespondents.itemsToAdd[0]
 | 
			
		||||
      messageFragment = `assign the correspondent ${correspondent.name} to`
 | 
			
		||||
    }
 | 
			
		||||
    modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.message = `This operation will ${messageFragment} all ${this.list.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
			
		||||
    modal.componentInstance.btnCaption = "Confirm"
 | 
			
		||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
			
		||||
      this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe(
 | 
			
		||||
        response => {
 | 
			
		||||
          this.correspondentService.clearCache()
 | 
			
		||||
          modal.close()
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setDocumentTypes(newDocumentTypesToggleableItems: ToggleableItem[]) {
 | 
			
		||||
    let changedDocumentTypes: ChangedItems
 | 
			
		||||
    if (newDocumentTypesToggleableItems) {
 | 
			
		||||
      changedDocumentTypes = this.checkForChangedItems(this.initialDocumentTypesToggleableItems, newDocumentTypesToggleableItems)
 | 
			
		||||
      if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
 | 
			
		||||
    }
 | 
			
		||||
  setDocumentTypes(changedDocumentTypes: ChangedItems) {
 | 
			
		||||
    if (changedDocumentTypes.itemsToAdd.length == 0 && changedDocumentTypes.itemsToRemove.length == 0) return
 | 
			
		||||
 | 
			
		||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
			
		||||
    modal.componentInstance.title = "Confirm Document Type Assignment"
 | 
			
		||||
@ -234,12 +142,13 @@ export class BulkEditorComponent {
 | 
			
		||||
      documentType = changedDocumentTypes.itemsToAdd[0]
 | 
			
		||||
      messageFragment = `assign the document type ${documentType.name} to`
 | 
			
		||||
    }
 | 
			
		||||
    modal.componentInstance.message = `This operation will ${messageFragment} all ${this.documentList.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.message = `This operation will ${messageFragment} all ${this.list.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.btnClass = "btn-warning"
 | 
			
		||||
    modal.componentInstance.btnCaption = "Confirm"
 | 
			
		||||
    modal.componentInstance.confirmClicked.subscribe(() => {
 | 
			
		||||
      this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe(
 | 
			
		||||
        response => {
 | 
			
		||||
          this.documentService.clearCache()
 | 
			
		||||
          modal.close()
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
@ -250,7 +159,7 @@ export class BulkEditorComponent {
 | 
			
		||||
    let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
 | 
			
		||||
    modal.componentInstance.delayConfirm(5)
 | 
			
		||||
    modal.componentInstance.title = "Delete confirm"
 | 
			
		||||
    modal.componentInstance.messageBold = `This operation will permanently delete all ${this.documentList.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.messageBold = `This operation will permanently delete all ${this.list.selected.size} selected document(s).`
 | 
			
		||||
    modal.componentInstance.message = `This operation cannot be undone.`
 | 
			
		||||
    modal.componentInstance.btnClass = "btn-danger"
 | 
			
		||||
    modal.componentInstance.btnCaption = "Delete document(s)"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user