mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-04-02 13:45:10 -05:00
Working bulk editor component
This commit is contained in:
parent
275bd96ba8
commit
24c53e78a7
@ -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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user