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() { | ||||
|     let iconName = '' | ||||
|     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 | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| <div class="btn-group" ngbDropdown role="group" (openChange)="dropdownOpenChange($event)" #dropdown="ngbDropdown"> | ||||
|   <button class="btn btn-sm" id="dropdown{{title}}" ngbDropdownToggle [ngClass]="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-inline-block d-md-none"> | ||||
|       <svg class="toolbaricon" fill="currentColor"> | ||||
|         <use attr.xlink:href="assets/bootstrap-icons.svg#{{icon}}" /> | ||||
|       </svg> | ||||
|     </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"> | ||||
|         {{itemsSelected?.length}} | ||||
|       </div> | ||||
|   | ||||
| @@ -17,6 +17,11 @@ export enum SelectableItemState { | ||||
|   PartiallySelected = 2 | ||||
| } | ||||
|  | ||||
| export enum FilterableDropdownType { | ||||
|   Filtering = 'filtering', | ||||
|   Actions = 'actions' | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-filterable-dropdown', | ||||
|   templateUrl: './filterable-dropdown.component.html', | ||||
| @@ -24,7 +29,10 @@ export enum SelectableItemState { | ||||
| }) | ||||
| export class FilterableDropdownComponent { | ||||
|  | ||||
|   constructor(private filterPipe: FilterPipe) { } | ||||
|   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef | ||||
|   @ViewChild('dropdown') dropdown: NgbDropdown | ||||
|  | ||||
|   filterText: string | ||||
|  | ||||
|   @Input() | ||||
|   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() | ||||
|   set itemsSelected(itemsSelected: ObjectWithId[]) { | ||||
| @@ -54,18 +72,26 @@ export class FilterableDropdownComponent { | ||||
|   @Input() | ||||
|   icon: string | ||||
|  | ||||
|   @Input() | ||||
|   type: FilterableDropdownType = FilterableDropdownType.Filtering | ||||
|  | ||||
|   @Input() | ||||
|   singular: boolean = false | ||||
|  | ||||
|   @Output() | ||||
|   toggle = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   close = new EventEmitter() | ||||
|  | ||||
|   @ViewChild('listFilterTextInput') listFilterTextInput: ElementRef | ||||
|   @ViewChild('dropdown') dropdown: NgbDropdown | ||||
|  | ||||
|   filterText: string | ||||
|   constructor(private filterPipe: FilterPipe) { } | ||||
|  | ||||
|   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) | ||||
|   } | ||||
|  | ||||
| @@ -76,7 +102,7 @@ export class FilterableDropdownComponent { | ||||
|       }, 0); | ||||
|     } else { | ||||
|       this.filterText = '' | ||||
|       this.close.next() | ||||
|       this.close.emit(this.itemsSelected) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -26,9 +26,18 @@ | ||||
|   <div class="col mb-2 mb-xl-0"> | ||||
|     <div class="d-flex"> | ||||
|       <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="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="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="Tags" icon="tag-fill" [selectableItems]="tagsSelectableItems" type="actions" (close)="applyTags($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="Document Type" icon="file-earmark-fill" [selectableItems]="documentTypesSelectableItems" type="actions" singular="true" (close)="applyDocumentType($event)"></app-filterable-dropdown> | ||||
|     </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> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { TagService } from 'src/app/services/rest/tag.service'; | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service'; | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service'; | ||||
| import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| import { SelectableItem, SelectableItemState } from 'src/app/components/common/filterable-dropdown/filterable-dropdown.component'; | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-bulk-editor', | ||||
| @@ -16,7 +17,7 @@ import { DocumentService } from 'src/app/services/rest/document.service'; | ||||
| export class BulkEditorComponent { | ||||
|  | ||||
|   @Input() | ||||
|   documentsSelected: Set<number> | ||||
|   selectedDocuments: Set<number> | ||||
|  | ||||
|   @Input() | ||||
|   allDocuments: PaperlessDocument[] | ||||
| @@ -33,20 +34,11 @@ export class BulkEditorComponent { | ||||
|   @Output() | ||||
|   setCorrespondent = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   removeCorrespondent = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   setDocumentType = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   removeDocumentType = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   addTag = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   removeTag = new EventEmitter() | ||||
|   setTags = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   delete = new EventEmitter() | ||||
| @@ -55,34 +47,47 @@ export class BulkEditorComponent { | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|  | ||||
|   get selectedTags(): PaperlessTag[] { | ||||
|     let selectedTags = [] | ||||
|     this.allDocuments.forEach(d => { | ||||
|       if (this.documentsSelected.has(d.id)) { | ||||
|         if (d.tags && !d.tags.every(t => selectedTags.find(st => st.id == t) !== undefined)) d.tags$.subscribe(t => selectedTags = selectedTags.concat(t)) | ||||
|       } | ||||
|   get tagsSelectableItems(): SelectableItem[] { | ||||
|     let tagsSelectableItems = [] | ||||
|     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||
|     this.tags.forEach(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[]  { | ||||
|     let selectedCorrespondents = [] | ||||
|     this.allDocuments.forEach(d => { | ||||
|       if (this.documentsSelected.has(d.id)) { | ||||
|         if (d.correspondent && selectedCorrespondents.find(sc => sc.id == d.correspondent) == undefined) d.correspondent$.subscribe(c => selectedCorrespondents.push(c)) | ||||
|       } | ||||
|   get correspondentsSelectableItems(): SelectableItem[] { | ||||
|     let correspondentsSelectableItems = [] | ||||
|     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||
|  | ||||
|     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[] { | ||||
|     let selectedDocumentTypes = [] | ||||
|     this.allDocuments.forEach(d => { | ||||
|       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)) | ||||
|       } | ||||
|   get documentTypesSelectableItems(): SelectableItem[] { | ||||
|     let documentTypesSelectableItems = [] | ||||
|     let selectedDocuments: PaperlessDocument[] = this.allDocuments.filter(d => this.selectedDocuments.has(d.id)) | ||||
|  | ||||
|     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( | ||||
| @@ -98,18 +103,19 @@ export class BulkEditorComponent { | ||||
|     this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results) | ||||
|   } | ||||
|  | ||||
|   applyTags(tags) { | ||||
|     console.log(tags); | ||||
|  | ||||
|   applyTags(tags: PaperlessTag[]) { | ||||
|     this.setTags.emit(tags) | ||||
|   } | ||||
|  | ||||
|   applyCorrespondent(correspondent) { | ||||
|     console.log(correspondent); | ||||
|  | ||||
|   applyCorrespondent(selectedCorrespondent: ObjectWithId[]) { | ||||
|     this.setCorrespondent.emit(selectedCorrespondent) | ||||
|   } | ||||
|  | ||||
|   applyDocumentType(documentType) { | ||||
|     console.log(documentType); | ||||
|   applyDocumentType(selectedDocumentType: ObjectWithId[]) { | ||||
|     this.setDocumentType.emit(selectedDocumentType) | ||||
|   } | ||||
|  | ||||
|   applyDelete() { | ||||
|     this.delete.next() | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -82,16 +82,13 @@ | ||||
|  | ||||
|   <app-bulk-editor *ngIf="isBulkEditing" | ||||
|     [allDocuments]="list.documents" | ||||
|     [(documentsSelected)]="list.selected" | ||||
|     [(selectedDocuments)]="list.selected" | ||||
|     (selectPage)="list.selectPage()" | ||||
|     (selectAll)="list.selectAll()" | ||||
|     (selectNone)="list.selectNone()" | ||||
|     (setCorrespondent)="bulkSetCorrespondent()" | ||||
|     (removeCorrespondent)="bulkRemoveCorrespondent()" | ||||
|     (setDocumentType)="bulkSetDocumentType()" | ||||
|     (removeDocumentType)="bulkRemoveDocumentType()" | ||||
|     (addTag)="bulkAddTag()" | ||||
|     (removeTag)="bulkRemoveTag()" | ||||
|     (setTags)="bulkSetTags($event)" | ||||
|     (setCorrespondent)="bulkSetCorrespondent($event)" | ||||
|     (setDocumentType)="bulkSetDocumentType($event)" | ||||
|     (delete)="bulkDelete()"> | ||||
| </app-bulk-editor> | ||||
| </div> | ||||
|   | ||||
| @@ -139,7 +139,9 @@ export class DocumentListComponent implements OnInit { | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   bulkSetCorrespondent() { | ||||
|   bulkSetCorrespondent(correspondent) { | ||||
|     console.log(correspondent); | ||||
|  | ||||
|     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.title = "Select correspondent" | ||||
|     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'}) | ||||
|     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):` | ||||
| @@ -193,6 +197,11 @@ export class DocumentListComponent implements OnInit { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   bulkSetTags(tags) { | ||||
|     console.log('bulkSetTags', tags); | ||||
|  | ||||
|   } | ||||
|  | ||||
|   bulkAddTag() { | ||||
|     let modal = this.modalService.open(SelectDialogComponent, {backdrop: 'static'}) | ||||
|     modal.componentInstance.title = "Select tag" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Michael Shamoon
					Michael Shamoon