mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #189 from shamoon/feature-bulk-editor
Refactored bulk editor
This commit is contained in:
		| @@ -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)="selectNone.next()"> |     <button class="btn btn-sm btn-outline-danger" (click)="documentList.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)="selectPage.next()"> |       <button class="btn btn-sm btn-outline-primary" (click)="documentList.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)="selectAll.next()"> |       <button class="btn btn-sm btn-outline-primary" (click)="documentList.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> | ||||||
| @@ -32,29 +32,29 @@ | |||||||
|       <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" |         [toggleableItems]="tagsToggleableItems" | ||||||
|         [type]="dropdownTypes.Editing" |         [type]="dropdownTypes.Editing" | ||||||
|         [showCounts]="!this.selectionSpansPages" |         [showCounts]="!selectionSpansPages" | ||||||
|         [showRemoveAll]="this.selectionSpansPages" |         [showRemoveAll]="selectionSpansPages" | ||||||
|         (open)="tagsDropdownOpen()" |         (open)="tagsDropdownOpen()" | ||||||
|         (removeAll)="removeAllTags()" |         (removeAll)="setTags(null)" | ||||||
|         (editingComplete)="applyTags($event)"> |         (editingComplete)="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" singular="true" | ||||||
|         [toggleableItems]="correspondentsToggleableItems" |         [toggleableItems]="correspondentsToggleableItems" | ||||||
|         [type]="dropdownTypes.Editing" |         [type]="dropdownTypes.Editing" | ||||||
|         [showCounts]="!this.selectionSpansPages" |         [showCounts]="!selectionSpansPages" | ||||||
|         [showRemoveAll]="this.selectionSpansPages" |         [showRemoveAll]="selectionSpansPages" | ||||||
|         (open)="correspondentsDropdownOpen()" |         (open)="correspondentsDropdownOpen()" | ||||||
|         (removeAll)="removeAllCorrespondents()" |         (removeAll)="setCorrespondents(null)" | ||||||
|         (editingComplete)="applyCorrespondent($event)"> |         (editingComplete)="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" singular="true" | ||||||
|         [toggleableItems]="documentTypesToggleableItems" |         [toggleableItems]="documentTypesToggleableItems" | ||||||
|         [type]="dropdownTypes.Editing" |         [type]="dropdownTypes.Editing" | ||||||
|         [showCounts]="!this.selectionSpansPages" |         [showCounts]="!selectionSpansPages" | ||||||
|         [showRemoveAll]="this.selectionSpansPages" |         [showRemoveAll]="selectionSpansPages" | ||||||
|         (open)="documentTypesDropdownOpen()" |         (open)="documentTypesDropdownOpen()" | ||||||
|         (removeAll)="removeAllDocumentTypes()" |         (removeAll)="setDocumentTypes(null)" | ||||||
|         (editingComplete)="applyDocumentType($event)"> |         (editingComplete)="setDocumentTypes($event)"> | ||||||
|       </app-filterable-dropdown> |       </app-filterable-dropdown> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|   | |||||||
| @@ -1,4 +1,6 @@ | |||||||
| import { Component, EventEmitter, Input, Output } from '@angular/core'; | import { Component, EventEmitter, Input, Output } from '@angular/core'; | ||||||
|  | import { Observable } from 'rxjs'; | ||||||
|  | import { tap } from 'rxjs/operators'; | ||||||
| import { ObjectWithId } from 'src/app/data/object-with-id'; | import { ObjectWithId } from 'src/app/data/object-with-id'; | ||||||
| import { PaperlessTag } from 'src/app/data/paperless-tag'; | import { PaperlessTag } from 'src/app/data/paperless-tag'; | ||||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'; | ||||||
| @@ -7,8 +9,12 @@ 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 { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | import { DocumentService } 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 { 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'; | import { ToggleableItem, ToggleableItemState } from 'src/app/components/common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'; | ||||||
|  |  | ||||||
| export interface ChangedItems { | export interface ChangedItems { | ||||||
| @@ -23,33 +29,6 @@ export interface ChangedItems { | |||||||
| }) | }) | ||||||
| export class BulkEditorComponent { | export class BulkEditorComponent { | ||||||
|  |  | ||||||
|   @Input() |  | ||||||
|   selectedDocuments: Set<number> |  | ||||||
|  |  | ||||||
|   @Input() |  | ||||||
|   viewDocuments: PaperlessDocument[] |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   selectPage = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   selectAll = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   selectNone = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   setTags = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   setCorrespondents = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   setDocumentTypes = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   delete = new EventEmitter() |  | ||||||
|  |  | ||||||
|   tags: PaperlessTag[] |   tags: PaperlessTag[] | ||||||
|   correspondents: PaperlessCorrespondent[] |   correspondents: PaperlessCorrespondent[] | ||||||
|   documentTypes: PaperlessDocumentType[] |   documentTypes: PaperlessDocumentType[] | ||||||
| @@ -61,13 +40,13 @@ export class BulkEditorComponent { | |||||||
|   dropdownTypes = FilterableDropdownType |   dropdownTypes = FilterableDropdownType | ||||||
|  |  | ||||||
|   get selectionSpansPages(): boolean { |   get selectionSpansPages(): boolean { | ||||||
|     return this.selectedDocuments.size > this.viewDocuments.length || !Array.from(this.selectedDocuments).every(sd => this.viewDocuments.find(d => d.id == sd)) |     return this.documentList.selected.size > this.documentList.documents.length || !Array.from(this.documentList.selected).every(sd => this.documentList.documents.find(d => d.id == sd)) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _tagsToggleableItems: ToggleableItem[] |   private _tagsToggleableItems: ToggleableItem[] | ||||||
|   get tagsToggleableItems(): ToggleableItem[] { |   get tagsToggleableItems(): ToggleableItem[] { | ||||||
|     let tagsToggleableItems = [] |     let tagsToggleableItems = [] | ||||||
|     let selectedDocuments: PaperlessDocument[] = this.viewDocuments.filter(d => this.selectedDocuments.has(d.id)) |     let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id)) | ||||||
|     if (this.selectionSpansPages) selectedDocuments = [] |     if (this.selectionSpansPages) selectedDocuments = [] | ||||||
|  |  | ||||||
|     this.tags?.forEach(t => { |     this.tags?.forEach(t => { | ||||||
| @@ -84,7 +63,7 @@ export class BulkEditorComponent { | |||||||
|   private _correspondentsToggleableItems: ToggleableItem[] |   private _correspondentsToggleableItems: ToggleableItem[] | ||||||
|   get correspondentsToggleableItems(): ToggleableItem[] { |   get correspondentsToggleableItems(): ToggleableItem[] { | ||||||
|     let correspondentsToggleableItems = [] |     let correspondentsToggleableItems = [] | ||||||
|     let selectedDocuments: PaperlessDocument[] = this.viewDocuments.filter(d => this.selectedDocuments.has(d.id)) |     let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id)) | ||||||
|     if (this.selectionSpansPages) selectedDocuments = [] |     if (this.selectionSpansPages) selectedDocuments = [] | ||||||
|  |  | ||||||
|     this.correspondents?.forEach(c => { |     this.correspondents?.forEach(c => { | ||||||
| @@ -101,7 +80,7 @@ export class BulkEditorComponent { | |||||||
|   private _documentTypesToggleableItems: ToggleableItem[] |   private _documentTypesToggleableItems: ToggleableItem[] | ||||||
|   get documentTypesToggleableItems(): ToggleableItem[] { |   get documentTypesToggleableItems(): ToggleableItem[] { | ||||||
|     let documentTypesToggleableItems = [] |     let documentTypesToggleableItems = [] | ||||||
|     let selectedDocuments: PaperlessDocument[] = this.viewDocuments.filter(d => this.selectedDocuments.has(d.id)) |     let selectedDocuments: PaperlessDocument[] = this.documentList.documents.filter(d => this.documentList.selected.has(d.id)) | ||||||
|     if (this.selectionSpansPages) selectedDocuments = [] |     if (this.selectionSpansPages) selectedDocuments = [] | ||||||
|  |  | ||||||
|     this.documentTypes?.forEach(dt => { |     this.documentTypes?.forEach(dt => { | ||||||
| @@ -115,11 +94,18 @@ export class BulkEditorComponent { | |||||||
|     return 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 documentService: DocumentService |     private documentListViewService: DocumentListViewService, | ||||||
|  |     private documentService: DocumentService, | ||||||
|  |     private modalService: NgbModal, | ||||||
|  |     private openDocumentService: OpenDocumentsService | ||||||
|   ) { } |   ) { } | ||||||
|  |  | ||||||
|   ngOnInit() { |   ngOnInit() { | ||||||
| @@ -140,34 +126,7 @@ export class BulkEditorComponent { | |||||||
|     this.initialDocumentTypesToggleableItems = this._documentTypesToggleableItems |     this.initialDocumentTypesToggleableItems = this._documentTypesToggleableItems | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyTags(newTagsToggleableItems: ToggleableItem[]) { |   private checkForChangedItems(toggleableItemsA: ToggleableItem[], toggleableItemsB: ToggleableItem[]): ChangedItems { | ||||||
|     let changedTags = this.checkForChangedItems(this.initialTagsToggleableItems, newTagsToggleableItems) |  | ||||||
|     if (changedTags.itemsToAdd.length > 0 || changedTags.itemsToRemove.length > 0) this.setTags.emit(changedTags) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   removeAllTags() { |  | ||||||
|     this.setTags.emit(null) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   applyCorrespondent(newCorrespondentsToggleableItems: ToggleableItem[]) { |  | ||||||
|     let changedCorrespondents = this.checkForChangedItems(this.initialCorrespondentsToggleableItems, newCorrespondentsToggleableItems) |  | ||||||
|     if (changedCorrespondents.itemsToAdd.length > 0 || changedCorrespondents.itemsToRemove.length > 0) this.setCorrespondents.emit(changedCorrespondents) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   removeAllCorrespondents() { |  | ||||||
|     this.setDocumentTypes.emit(null) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   applyDocumentType(newDocumentTypesToggleableItems: ToggleableItem[]) { |  | ||||||
|     let changedDocumentTypes = this.checkForChangedItems(this.initialDocumentTypesToggleableItems, newDocumentTypesToggleableItems) |  | ||||||
|     if (changedDocumentTypes.itemsToAdd.length > 0 || changedDocumentTypes.itemsToRemove.length > 0) this.setDocumentTypes.emit(changedDocumentTypes) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   removeAllDocumentTypes() { |  | ||||||
|     this.setDocumentTypes.emit(null) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   checkForChangedItems(toggleableItemsA: ToggleableItem[], toggleableItemsB: ToggleableItem[]): ChangedItems { |  | ||||||
|     let itemsToAdd: any[] = [] |     let itemsToAdd: any[] = [] | ||||||
|     let itemsToRemove: any[] = [] |     let itemsToRemove: any[] = [] | ||||||
|     toggleableItemsA.forEach(oldItem => { |     toggleableItemsA.forEach(oldItem => { | ||||||
| @@ -179,7 +138,135 @@ export class BulkEditorComponent { | |||||||
|     return { itemsToAdd: itemsToAdd, itemsToRemove: itemsToRemove } |     return { itemsToAdd: itemsToAdd, itemsToRemove: itemsToRemove } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private executeBulkOperation(method: string, args): Observable<any> { | ||||||
|  |     return this.documentService.bulkEdit(Array.from(this.documentList.selected), method, args).pipe( | ||||||
|  |       tap(() => { | ||||||
|  |         this.documentList.reload() | ||||||
|  |         this.documentList.selected.forEach(id => { | ||||||
|  |           this.openDocumentService.refreshDocument(id) | ||||||
|  |         }) | ||||||
|  |         this.documentList.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 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     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() | ||||||
|  |               }) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   setCorrespondents(newCorrespondentsToggleableItems: ToggleableItem[]) { | ||||||
|  |     let changedCorrespondents: ChangedItems | ||||||
|  |     if (newCorrespondentsToggleableItems) { | ||||||
|  |       changedCorrespondents = this.checkForChangedItems(this.initialCorrespondentsToggleableItems, newCorrespondentsToggleableItems) | ||||||
|  |       if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) | ||||||
|  |     modal.componentInstance.title = "Confirm Correspondent Assignment" | ||||||
|  |     let correspondent | ||||||
|  |     let messageFragment = 'remove all correspondents from' | ||||||
|  |     if (changedCorrespondents && changedCorrespondents.itemsToAdd.length > 0) { | ||||||
|  |       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.btnClass = "btn-warning" | ||||||
|  |     modal.componentInstance.btnCaption = "Confirm" | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       this.executeBulkOperation('set_correspondent', {"correspondent": correspondent ? correspondent.id : null}).subscribe( | ||||||
|  |         response => { | ||||||
|  |           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 | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) | ||||||
|  |     modal.componentInstance.title = "Confirm Document Type Assignment" | ||||||
|  |     let documentType | ||||||
|  |     let messageFragment = 'remove all document types from' | ||||||
|  |     if (changedDocumentTypes && changedDocumentTypes.itemsToAdd.length > 0) { | ||||||
|  |       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.btnClass = "btn-warning" | ||||||
|  |     modal.componentInstance.btnCaption = "Confirm" | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       this.executeBulkOperation('set_document_type', {"document_type": documentType ? documentType.id : null}).subscribe( | ||||||
|  |         response => { | ||||||
|  |           modal.close() | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  |  | ||||||
|   applyDelete() { |   applyDelete() { | ||||||
|     this.delete.next() |     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.message = `This operation cannot be undone.` | ||||||
|  |     modal.componentInstance.btnClass = "btn-danger" | ||||||
|  |     modal.componentInstance.btnCaption = "Delete document(s)" | ||||||
|  |     modal.componentInstance.confirmClicked.subscribe(() => { | ||||||
|  |       this.executeBulkOperation("delete", {}).subscribe( | ||||||
|  |         response => { | ||||||
|  |           modal.close() | ||||||
|  |         } | ||||||
|  |       ) | ||||||
|  |     }) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -79,18 +79,7 @@ | |||||||
|  |  | ||||||
| <div class="w-100 mb-2 mb-sm-4"> | <div class="w-100 mb-2 mb-sm-4"> | ||||||
|   <app-filter-editor *ngIf="!isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> |   <app-filter-editor *ngIf="!isBulkEditing" [(filterRules)]="list.filterRules" #filterEditor></app-filter-editor> | ||||||
|  |   <app-bulk-editor *ngIf="isBulkEditing"></app-bulk-editor> | ||||||
|   <app-bulk-editor *ngIf="isBulkEditing" |  | ||||||
|     [viewDocuments]="list.documents" |  | ||||||
|     [(selectedDocuments)]="list.selected" |  | ||||||
|     (selectPage)="list.selectPage()" |  | ||||||
|     (selectAll)="list.selectAll()" |  | ||||||
|     (selectNone)="list.selectNone()" |  | ||||||
|     (setTags)="bulkSetTags($event)" |  | ||||||
|     (setCorrespondents)="bulkSetCorrespondents($event)" |  | ||||||
|     (setDocumentTypes)="bulkSetDocumentTypes($event)" |  | ||||||
|     (delete)="bulkDelete()"> |  | ||||||
| </app-bulk-editor> |  | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <div class="d-flex justify-content-between align-items-center"> | <div class="d-flex justify-content-between align-items-center"> | ||||||
|   | |||||||
| @@ -1,23 +1,19 @@ | |||||||
| import { Component, OnInit, ViewChild } from '@angular/core'; | import { Component, OnInit, ViewChild } from '@angular/core'; | ||||||
| import { ActivatedRoute, Router } from '@angular/router'; | import { ActivatedRoute, Router } from '@angular/router'; | ||||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; | ||||||
| import { Observable } from 'rxjs'; |  | ||||||
| import { tap } from 'rxjs/operators'; |  | ||||||
| import { PaperlessDocument } from 'src/app/data/paperless-document'; | import { PaperlessDocument } from 'src/app/data/paperless-document'; | ||||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; | import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'; | ||||||
| import { DocumentListViewService } from 'src/app/services/document-list-view.service'; | import { DocumentListViewService } from 'src/app/services/document-list-view.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 { DocumentService, DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; | import { DOCUMENT_SORT_FIELDS } from 'src/app/services/rest/document.service'; | ||||||
| import { TagService } from 'src/app/services/rest/tag.service'; | import { TagService } from 'src/app/services/rest/tag.service'; | ||||||
| import { SavedViewService } from 'src/app/services/rest/saved-view.service'; | import { SavedViewService } from 'src/app/services/rest/saved-view.service'; | ||||||
| import { Toast, ToastService } from 'src/app/services/toast.service'; | import { Toast, ToastService } from 'src/app/services/toast.service'; | ||||||
| import { FilterEditorComponent } from './filter-editor/filter-editor.component'; | import { FilterEditorComponent } from './filter-editor/filter-editor.component'; | ||||||
| import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'; |  | ||||||
| import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component'; | import { SelectDialogComponent } from '../common/select-dialog/select-dialog.component'; | ||||||
| import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; | import { SaveViewConfigDialogComponent } from './save-view-config-dialog/save-view-config-dialog.component'; | ||||||
| import { ChangedItems } from './bulk-editor/bulk-editor.component'; | import { ChangedItems } from './bulk-editor/bulk-editor.component'; | ||||||
| import { OpenDocumentsService } from 'src/app/services/open-documents.service'; |  | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-document-list', |   selector: 'app-document-list', | ||||||
| @@ -32,12 +28,10 @@ export class DocumentListComponent implements OnInit { | |||||||
|     public route: ActivatedRoute, |     public route: ActivatedRoute, | ||||||
|     private router: Router, |     private router: Router, | ||||||
|     private toastService: ToastService, |     private toastService: ToastService, | ||||||
|     public modalService: NgbModal, |     private modalService: NgbModal, | ||||||
|     private correspondentService: CorrespondentService, |     private correspondentService: CorrespondentService, | ||||||
|     private documentTypeService: DocumentTypeService, |     private documentTypeService: DocumentTypeService, | ||||||
|     private tagService: TagService, |     private tagService: TagService) { } | ||||||
|     private documentService: DocumentService, |  | ||||||
|     private openDocumentService: OpenDocumentsService) { } |  | ||||||
|  |  | ||||||
|   @ViewChild("filterEditor") |   @ViewChild("filterEditor") | ||||||
|   private filterEditor: FilterEditorComponent |   private filterEditor: FilterEditorComponent | ||||||
| @@ -143,118 +137,4 @@ export class DocumentListComponent implements OnInit { | |||||||
|   trackByDocumentId(index, item: PaperlessDocument) { |   trackByDocumentId(index, item: PaperlessDocument) { | ||||||
|     return item.id |     return item.id | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private executeBulkOperation(method: string, args): Observable<any> { |  | ||||||
|     return this.documentService.bulkEdit(Array.from(this.list.selected), method, args).pipe( |  | ||||||
|       tap(() => { |  | ||||||
|         this.list.reload() |  | ||||||
|         this.list.selected.forEach(id => { |  | ||||||
|           this.openDocumentService.refreshDocument(id) |  | ||||||
|         }) |  | ||||||
|         this.list.selectNone() |  | ||||||
|       }) |  | ||||||
|     ) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bulkSetTags(changedTags: ChangedItems) { |  | ||||||
|     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.list.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() |  | ||||||
|               }) |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bulkSetCorrespondents(changedCorrespondents: ChangedItems) { |  | ||||||
|     let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) |  | ||||||
|     modal.componentInstance.title = "Confirm Correspondent Assignment" |  | ||||||
|     let correspondent |  | ||||||
|     let messageFragment = 'remove all correspondents from' |  | ||||||
|     if (changedCorrespondents && changedCorrespondents.itemsToAdd.length > 0) { |  | ||||||
|       correspondent = changedCorrespondents.itemsToAdd[0] |  | ||||||
|       messageFragment = `assign the correspondent ${correspondent.name} to` |  | ||||||
|     } |  | ||||||
|     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 => { |  | ||||||
|           modal.close() |  | ||||||
|         } |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bulkSetDocumentTypes(changedDocumentTypes: ChangedItems) { |  | ||||||
|     let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'}) |  | ||||||
|     modal.componentInstance.title = "Confirm Document Type Assignment" |  | ||||||
|     let documentType |  | ||||||
|     let messageFragment = 'remove all document types from' |  | ||||||
|     if (changedDocumentTypes && changedDocumentTypes.itemsToAdd.length > 0) { |  | ||||||
|       documentType = changedDocumentTypes.itemsToAdd[0] |  | ||||||
|       messageFragment = `assign the document type ${documentType.name} to` |  | ||||||
|     } |  | ||||||
|     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 => { |  | ||||||
|           modal.close() |  | ||||||
|         } |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bulkDelete() { |  | ||||||
|     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.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)" |  | ||||||
|     modal.componentInstance.confirmClicked.subscribe(() => { |  | ||||||
|       this.executeBulkOperation("delete", {}).subscribe( |  | ||||||
|         response => { |  | ||||||
|           modal.close() |  | ||||||
|         } |  | ||||||
|       ) |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Jonas Winkler
					Jonas Winkler