mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Working bulk editor component
This commit is contained in:
		| @@ -35,7 +35,7 @@ export class FilterableDropdownButtonComponent implements OnInit { | |||||||
|   getSelectedIconName() { |   getSelectedIconName() { | ||||||
|     let iconName = '' |     let iconName = '' | ||||||
|     if (this.selectableItem?.state == SelectableItemState.Selected) iconName = 'check' |     if (this.selectableItem?.state == SelectableItemState.Selected) iconName = 'check' | ||||||
|     else if (this.selectableItem?.state == SelectableItemState.PartiallySelected) iconName = 'minus' |     else if (this.selectableItem?.state == SelectableItemState.PartiallySelected) iconName = 'dash' | ||||||
|     return iconName |     return iconName | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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]="itemsSelected?.length > 0 ? 'btn-primary' : 'btn-outline-primary'"> |   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="type !== 'actions' && itemsSelected?.length > 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="itemsSelected?.length > 0"> |     <ng-container *ngIf="type !== 'actions' && itemsSelected?.length > 0"> | ||||||
|       <div class="badge bg-secondary text-light rounded-pill badge-corner"> |       <div class="badge bg-secondary text-light rounded-pill badge-corner"> | ||||||
|         {{itemsSelected?.length}} |         {{itemsSelected?.length}} | ||||||
|       </div> |       </div> | ||||||
|   | |||||||
| @@ -17,6 +17,11 @@ export enum SelectableItemState { | |||||||
|   PartiallySelected = 2 |   PartiallySelected = 2 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export enum FilterableDropdownType { | ||||||
|  |   Filtering = 'filtering', | ||||||
|  |   Actions = 'actions' | ||||||
|  | } | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-filterable-dropdown', |   selector: 'app-filterable-dropdown', | ||||||
|   templateUrl: './filterable-dropdown.component.html', |   templateUrl: './filterable-dropdown.component.html', | ||||||
| @@ -24,7 +29,10 @@ export enum SelectableItemState { | |||||||
| }) | }) | ||||||
| export class FilterableDropdownComponent { | export class FilterableDropdownComponent { | ||||||
|  |  | ||||||
|   constructor(private filterPipe: FilterPipe) { } |   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef | ||||||
|  |   @ViewChild('dropdown') dropdown: NgbDropdown | ||||||
|  |  | ||||||
|  |   filterText: string | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   set items(items: ObjectWithId[]) { |   set items(items: ObjectWithId[]) { | ||||||
| @@ -35,7 +43,17 @@ export class FilterableDropdownComponent { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   selectableItems: SelectableItem[] = [] |   _selectableItems: SelectableItem[] = [] | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   set selectableItems (selectableItems: SelectableItem[]) { | ||||||
|  |     if (this.type == FilterableDropdownType.Actions && this.dropdown?.isOpen()) return | ||||||
|  |     else this._selectableItems = selectableItems | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   get selectableItems(): SelectableItem[] { | ||||||
|  |     return this._selectableItems | ||||||
|  |   } | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   set itemsSelected(itemsSelected: ObjectWithId[]) { |   set itemsSelected(itemsSelected: ObjectWithId[]) { | ||||||
| @@ -54,18 +72,26 @@ export class FilterableDropdownComponent { | |||||||
|   @Input() |   @Input() | ||||||
|   icon: string |   icon: string | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   type: FilterableDropdownType = FilterableDropdownType.Filtering | ||||||
|  |  | ||||||
|  |   @Input() | ||||||
|  |   singular: boolean = false | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   toggle = new EventEmitter() |   toggle = new EventEmitter() | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   close = new EventEmitter() |   close = new EventEmitter() | ||||||
|  |  | ||||||
|   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef |   constructor(private filterPipe: FilterPipe) { } | ||||||
|   @ViewChild('dropdown') dropdown: NgbDropdown |  | ||||||
|  |  | ||||||
|   filterText: string |  | ||||||
|  |  | ||||||
|   toggleItem(selectableItem: SelectableItem): void { |   toggleItem(selectableItem: SelectableItem): void { | ||||||
|  |     if (this.singular && selectableItem.state == SelectableItemState.Selected) { | ||||||
|  |       this._selectableItems.forEach(si => { | ||||||
|  |         if (si.state == SelectableItemState.Selected && si.item.id !== selectableItem.item.id) si.state = SelectableItemState.NotSelected | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|     this.toggle.emit(selectableItem.item) |     this.toggle.emit(selectableItem.item) | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -76,7 +102,7 @@ export class FilterableDropdownComponent { | |||||||
|       }, 0); |       }, 0); | ||||||
|     } else { |     } else { | ||||||
|       this.filterText = '' |       this.filterText = '' | ||||||
|       this.close.next() |       this.close.emit(this.itemsSelected) | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,9 +26,18 @@ | |||||||
|   <div class="col mb-2 mb-xl-0"> |   <div class="col mb-2 mb-xl-0"> | ||||||
|     <div class="d-flex"> |     <div class="d-flex"> | ||||||
|       <label class="ml-auto mt-1 mr-2">Apply:</label> |       <label class="ml-auto mt-1 mr-2">Apply:</label> | ||||||
|       <app-filterable-dropdown class="mr-2 mr-md-3" title="Apply Tags" icon="tag-fill" [items]="tags" [itemsSelected]="selectedTags" (close)="applyTags($event)"></app-filterable-dropdown> |       <app-filterable-dropdown class="mr-2 mr-md-3" title="Tags" icon="tag-fill" [selectableItems]="tagsSelectableItems" type="actions" (close)="applyTags($event)"></app-filterable-dropdown> | ||||||
|       <app-filterable-dropdown class="mr-2 mr-md-3" title="Set Correspondent" icon="person-fill" [items]="correspondents" [itemsSelected]="selectedCorrespondents" (close)="applyCorrespondent($event)"></app-filterable-dropdown> |       <app-filterable-dropdown class="mr-2 mr-md-3" title="Correspondent" icon="person-fill" [selectableItems]="correspondentsSelectableItems" type="actions" singular="true" (close)="applyCorrespondent($event)"></app-filterable-dropdown> | ||||||
|       <app-filterable-dropdown class="mr-2 mr-md-3" title="Set Document Type" icon="file-earmark-fill" [items]="documentTypes" [itemsSelected]="selectedDocumentTypes" (close)="applyDocumentType($event)"></app-filterable-dropdown> |       <app-filterable-dropdown class="mr-2 mr-md-3" title="Document Type" icon="file-earmark-fill" [selectableItems]="documentTypesSelectableItems" type="actions" singular="true" (close)="applyDocumentType($event)"></app-filterable-dropdown> | ||||||
|     </div> |     </div> | ||||||
|   </div> |   </div> | ||||||
|  |   <div class="w-100 d-xl-none"></div> | ||||||
|  |   <div class="col-auto mb-2 mb-xl-0"> | ||||||
|  |     <button type="button" class="btn btn-sm btn-outline-danger " (click)="applyDelete()"> | ||||||
|  |       <svg class="buttonicon" fill="currentColor"> | ||||||
|  |         <use xlink:href="assets/bootstrap-icons.svg#trash" /> | ||||||
|  |       </svg> | ||||||
|  |       <span class="d-none d-lg-inline"> Delete</span> | ||||||
|  |     </button> | ||||||
|  |   </div> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ 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 { DocumentService } from 'src/app/services/rest/document.service'; | import { DocumentService } from 'src/app/services/rest/document.service'; | ||||||
|  | import { SelectableItem, SelectableItemState } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component'; | ||||||
|  |  | ||||||
| @Component({ | @Component({ | ||||||
|   selector: 'app-bulk-editor', |   selector: 'app-bulk-editor', | ||||||
| @@ -16,7 +17,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'; | |||||||
| export class BulkEditorComponent { | export class BulkEditorComponent { | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   documentsSelected: Set<number> |   selectedDocuments: Set<number> | ||||||
|  |  | ||||||
|   @Input() |   @Input() | ||||||
|   allDocuments: PaperlessDocument[] |   allDocuments: PaperlessDocument[] | ||||||
| @@ -33,20 +34,11 @@ export class BulkEditorComponent { | |||||||
|   @Output() |   @Output() | ||||||
|   setCorrespondent = new EventEmitter() |   setCorrespondent = new EventEmitter() | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   removeCorrespondent = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   setDocumentType = new EventEmitter() |   setDocumentType = new EventEmitter() | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   removeDocumentType = new EventEmitter() |   setTags = new EventEmitter() | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   addTag = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |  | ||||||
|   removeTag = new EventEmitter() |  | ||||||
|  |  | ||||||
|   @Output() |   @Output() | ||||||
|   delete = new EventEmitter() |   delete = new EventEmitter() | ||||||
| @@ -55,34 +47,47 @@ export class BulkEditorComponent { | |||||||
|   correspondents: PaperlessCorrespondent[] |   correspondents: PaperlessCorrespondent[] | ||||||
|   documentTypes: PaperlessDocumentType[] |   documentTypes: PaperlessDocumentType[] | ||||||
|  |  | ||||||
|   get selectedTags(): PaperlessTag[] { |   get tagsSelectableItems(): SelectableItem[] { | ||||||
|     let selectedTags = [] |     let tagsSelectableItems = [] | ||||||
|     this.allDocuments.forEach(d => { |     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||||
|       if (this.documentsSelected.has(d.id)) { |     this.tags.forEach(t => { | ||||||
|         if (d.tags && !d.tags.every(t => selectedTags.find(st => st.id == t) !== undefined)) d.tags$.subscribe(t => selectedTags = selectedTags.concat(t)) |       let selectedDocumentsWithTag: PaperlessDocument[] = selectedDocuments.filter(d => d.tags.includes(t.id)) | ||||||
|       } |       let state = SelectableItemState.NotSelected | ||||||
|  |       if (selectedDocumentsWithTag.length == selectedDocuments.length) state = SelectableItemState.Selected | ||||||
|  |       else if (selectedDocumentsWithTag.length > 0 && selectedDocumentsWithTag.length < selectedDocuments.length) state = SelectableItemState.PartiallySelected | ||||||
|  |       tagsSelectableItems.push( { item: t, state: state } ) | ||||||
|     }) |     }) | ||||||
|     return selectedTags |     return tagsSelectableItems | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get selectedCorrespondents(): PaperlessCorrespondent[]  { |   get correspondentsSelectableItems(): SelectableItem[] { | ||||||
|     let selectedCorrespondents = [] |     let correspondentsSelectableItems = [] | ||||||
|     this.allDocuments.forEach(d => { |     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||||
|       if (this.documentsSelected.has(d.id)) { |  | ||||||
|         if (d.correspondent && selectedCorrespondents.find(sc => sc.id == d.correspondent) == undefined) d.correspondent$.subscribe(c => selectedCorrespondents.push(c)) |     this.correspondents.forEach(c => { | ||||||
|       } |       let selectedDocumentsWithCorrespondent: PaperlessDocument[] = selectedDocuments.filter(d => d.correspondent == c.id) | ||||||
|  |       let state = SelectableItemState.NotSelected | ||||||
|  |       if (selectedDocumentsWithCorrespondent.length == selectedDocuments.length) state = SelectableItemState.Selected | ||||||
|  |       else if (selectedDocumentsWithCorrespondent.length > 0 && selectedDocumentsWithCorrespondent.length < selectedDocuments.length) state = SelectableItemState.PartiallySelected | ||||||
|  |       correspondentsSelectableItems.push( { item: c, state: state } ) | ||||||
|     }) |     }) | ||||||
|     return selectedCorrespondents |  | ||||||
|  |     return correspondentsSelectableItems | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   get selectedDocumentTypes(): PaperlessDocumentType[] { |   get documentTypesSelectableItems(): SelectableItem[] { | ||||||
|     let selectedDocumentTypes = [] |     let documentTypesSelectableItems = [] | ||||||
|     this.allDocuments.forEach(d => { |     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||||
|       if (this.documentsSelected.has(d.id)) { |  | ||||||
|         if (d.document_type && selectedDocumentTypes.find(sdt => sdt.id == d.document_type) == undefined) d.document_type$.subscribe(dt => selectedDocumentTypes.push(dt)) |     this.documentTypes.forEach(dt => { | ||||||
|       } |       let selectedDocumentsWithDocumentType: PaperlessDocument[] = selectedDocuments.filter(d => d.document_type == dt.id) | ||||||
|  |       let state = SelectableItemState.NotSelected | ||||||
|  |       if (selectedDocumentsWithDocumentType.length == selectedDocuments.length) state = SelectableItemState.Selected | ||||||
|  |       else if (selectedDocumentsWithDocumentType.length > 0 && selectedDocumentsWithDocumentType.length < selectedDocuments.length) state = SelectableItemState.PartiallySelected | ||||||
|  |       documentTypesSelectableItems.push( { item: dt, state: state } ) | ||||||
|     }) |     }) | ||||||
|     return selectedDocumentTypes |  | ||||||
|  |     return documentTypesSelectableItems | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
| @@ -98,18 +103,19 @@ export class BulkEditorComponent { | |||||||
|     this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) |     this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyTags(tags) { |   applyTags(tags: PaperlessTag[]) { | ||||||
|     console.log(tags); |     this.setTags.emit(tags) | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyCorrespondent(correspondent) { |   applyCorrespondent(selectedCorrespondent: ObjectWithId[]) { | ||||||
|     console.log(correspondent); |     this.setCorrespondent.emit(selectedCorrespondent) | ||||||
|  |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   applyDocumentType(documentType) { |   applyDocumentType(selectedDocumentType: ObjectWithId[]) { | ||||||
|     console.log(documentType); |     this.setDocumentType.emit(selectedDocumentType) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   applyDelete() { | ||||||
|  |     this.delete.next() | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -82,16 +82,13 @@ | |||||||
|  |  | ||||||
|   <app-bulk-editor *ngIf="isBulkEditing" |   <app-bulk-editor *ngIf="isBulkEditing" | ||||||
|     [allDocuments]="list.documents" |     [allDocuments]="list.documents" | ||||||
|     [(documentsSelected)]="list.selected" |     [(selectedDocuments)]="list.selected" | ||||||
|     (selectPage)="list.selectPage()" |     (selectPage)="list.selectPage()" | ||||||
|     (selectAll)="list.selectAll()" |     (selectAll)="list.selectAll()" | ||||||
|     (selectNone)="list.selectNone()" |     (selectNone)="list.selectNone()" | ||||||
|     (setCorrespondent)="bulkSetCorrespondent()" |     (setTags)="bulkSetTags($event)" | ||||||
|     (removeCorrespondent)="bulkRemoveCorrespondent()" |     (setCorrespondent)="bulkSetCorrespondent($event)" | ||||||
|     (setDocumentType)="bulkSetDocumentType()" |     (setDocumentType)="bulkSetDocumentType($event)" | ||||||
|     (removeDocumentType)="bulkRemoveDocumentType()" |  | ||||||
|     (addTag)="bulkAddTag()" |  | ||||||
|     (removeTag)="bulkRemoveTag()" |  | ||||||
|     (delete)="bulkDelete()"> |     (delete)="bulkDelete()"> | ||||||
| </app-bulk-editor> | </app-bulk-editor> | ||||||
| </div> | </div> | ||||||
|   | |||||||
| @@ -139,7 +139,9 @@ export class DocumentListComponent implements OnInit { | |||||||
|     ) |     ) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bulkSetCorrespondent() { |   bulkSetCorrespondent(correspondent) { | ||||||
|  |     console.log(correspondent); | ||||||
|  |  | ||||||
|     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) |     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) | ||||||
|     modal.componentInstance.title = "Select correspondent" |     modal.componentInstance.title = "Select correspondent" | ||||||
|     modal.componentInstance.message = `Select the correspondent you wish to assign to ${this.list.selected.size} selected document(s):` |     modal.componentInstance.message = `Select the correspondent you wish to assign to ${this.list.selected.size} selected document(s):` | ||||||
| @@ -166,7 +168,9 @@ export class DocumentListComponent implements OnInit { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bulkSetDocumentType() { |   bulkSetDocumentType(documentType) { | ||||||
|  |     console.log(); | ||||||
|  |  | ||||||
|     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) |     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) | ||||||
|     modal.componentInstance.title = "Select document type" |     modal.componentInstance.title = "Select document type" | ||||||
|     modal.componentInstance.message = `Select the document type you wish to assign to ${this.list.selected.size} selected document(s):` |     modal.componentInstance.message = `Select the document type you wish to assign to ${this.list.selected.size} selected document(s):` | ||||||
| @@ -193,6 +197,11 @@ export class DocumentListComponent implements OnInit { | |||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   bulkSetTags(tags) { | ||||||
|  |     console.log('bulkSetTags', tags); | ||||||
|  |  | ||||||
|  |   } | ||||||
|  |  | ||||||
|   bulkAddTag() { |   bulkAddTag() { | ||||||
|     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) |     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) | ||||||
|     modal.componentInstance.title = "Select tag" |     modal.componentInstance.title = "Select tag" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon