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