diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
index 7c9a133ce..d7b2af6d3 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html
@@ -1,12 +1,12 @@
-
-
- 0}">Apply
+
+ Apply
diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
index acb07e0d4..8fb2d25d9 100644
--- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
+++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts
@@ -1,14 +1,13 @@
import { Component, EventEmitter, Input, Output, ElementRef, ViewChild } from '@angular/core';
import { FilterPipe } from 'src/app/pipes/filter.pipe';
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 { Subject } from 'rxjs';
-import { ThrowStmt } from '@angular/compiler';
-export enum FilterableDropdownType {
- Filtering = 'filtering',
- Editing = 'editing'
+export interface ChangedItems {
+ itemsToAdd: MatchingModel[],
+ itemsToRemove: MatchingModel[]
}
export class FilterableDropdownSelectionModel {
@@ -19,31 +18,37 @@ export class FilterableDropdownSelectionModel {
items: MatchingModel[] = []
- selection = new Map()
+ private selectionStates = new Map()
+
+ private temporarySelectionStates = new Map()
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) {
- this.selection.set(id, state)
+ if (state == ToggleableItemState.NotSelected) {
+ this.temporarySelectionStates.delete(id)
+ } else {
+ this.temporarySelectionStates.set(id, state)
+ }
if (fireEvent) {
this.changed.next(this)
}
}
toggle(id: number, fireEvent = true) {
- let state = this.selection.get(id)
+ let state = this.temporarySelectionStates.get(id)
if (state == null || state != ToggleableItemState.Selected) {
- this.selection.set(id, ToggleableItemState.Selected)
+ this.temporarySelectionStates.set(id, ToggleableItemState.Selected)
} else if (state == ToggleableItemState.Selected) {
- this.selection.set(id, ToggleableItemState.NotSelected)
+ this.temporarySelectionStates.delete(id)
}
if (!this.multiple) {
- for (let key of this.selection.keys()) {
+ for (let key of this.temporarySelectionStates.keys()) {
if (key != id) {
- this.selection.set(key, ToggleableItemState.NotSelected)
+ this.temporarySelectionStates.delete(key)
}
}
}
@@ -55,7 +60,7 @@ export class FilterableDropdownSelectionModel {
}
get(id: number) {
- return this.selection.get(id) || ToggleableItemState.NotSelected
+ return this.temporarySelectionStates.get(id) || ToggleableItemState.NotSelected
}
selectionSize() {
@@ -63,11 +68,47 @@ export class FilterableDropdownSelectionModel {
}
clear(fireEvent = true) {
- this.selection.clear()
+ this.temporarySelectionStates.clear()
if (fireEvent) {
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({
@@ -131,35 +172,35 @@ export class FilterableDropdownComponent {
icon: string
@Input()
- type: FilterableDropdownType = FilterableDropdownType.Filtering
+ editing = false
- types = FilterableDropdownType
+ @Output()
+ apply = new EventEmitter()
- hasBeenToggled:boolean = false
+ @Output()
+ open = new EventEmitter()
constructor(private filterPipe: FilterPipe) {
this.selectionModel = new FilterableDropdownSelectionModel()
}
- toggleItem(toggleableItem: ToggleableItem): void {
- // if (this.singular && toggleableItem.state == ToggleableItemState.Selected) {
- // this.selectionModel.items.filter(ti => ti.item.id !== toggleableItem.item.id).forEach(ti => ti.state = ToggleableItemState.NotSelected)
- // }
- // this.hasBeenToggled = true
- // this.toggle.emit(toggleableItem.item)
+ applyClicked() {
+ if (this.selectionModel.isDirty()) {
+ this.dropdown.close()
+ this.apply.emit(this.selectionModel.diff())
+ }
}
dropdownOpenChange(open: boolean): void {
- // if (open) {
- // setTimeout(() => {
- // this.listFilterTextInput.nativeElement.focus();
- // }, 0)
- // this.hasBeenToggled = false
- // this.open.next()
- // } else {
- // this.filterText = ''
- // if (this.type == FilterableDropdownType.Editing) this.editingComplete.emit(this.toggleableItems)
- // }
+ if (open) {
+ setTimeout(() => {
+ this.listFilterTextInput.nativeElement.focus();
+ }, 0)
+ this.selectionModel.reset()
+ this.open.next()
+ } else {
+ this.filterText = ''
+ }
}
listFilterEnter(): void {
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
index 800ef3742..c78f3ea3e 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.html
@@ -1,6 +1,6 @@
-
+
diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
index 4365e00b0..36577bc6f 100644
--- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
+++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -1,26 +1,19 @@
-import { Component, EventEmitter, Input, Output } from '@angular/core';
+import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
-import { ObjectWithId } from 'src/app/data/object-with-id';
import { PaperlessTag } from 'src/app/data/paperless-tag';
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent';
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 { CorrespondentService } from 'src/app/services/rest/correspondent.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, SelectionDataItem } 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 { 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';
-
-export interface ChangedItems {
- itemsToAdd: any[],
- itemsToRemove: any[]
-}
+import { ChangedItems, FilterableDropdownSelectionModel } from '../../common/filterable-dropdown/filterable-dropdown.component';
+import { ToggleableItemState } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component';
@Component({
selector: 'app-bulk-editor',
@@ -33,69 +26,15 @@ export class BulkEditorComponent {
correspondents: PaperlessCorrespondent[]
documentTypes: PaperlessDocumentType[]
- private initialTagsToggleableItems: ToggleableItem[]
- private initialCorrespondentsToggleableItems: ToggleableItem[]
- private initialDocumentTypesToggleableItems: ToggleableItem[]
-
- 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
- }
+ tagSelectionModel = new FilterableDropdownSelectionModel()
+ correspondentSelectionModel = new FilterableDropdownSelectionModel()
+ documentTypeSelectionModel = new FilterableDropdownSelectionModel()
constructor(
private documentTypeService: DocumentTypeService,
private tagService: TagService,
private correspondentService: CorrespondentService,
- private documentListViewService: DocumentListViewService,
+ public list: DocumentListViewService,
private documentService: DocumentService,
private modalService: NgbModal,
private openDocumentService: OpenDocumentsService
@@ -107,97 +46,69 @@ export class BulkEditorComponent {
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 {
- 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(() => {
- this.documentList.reload()
- this.documentList.selected.forEach(id => {
+ this.list.reload()
+ this.list.selected.forEach(id => {
this.openDocumentService.refreshDocument(id)
})
- this.documentList.selectNone()
+ this.list.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
- }
+ private applySelectionData(items: SelectionDataItem[], selectionModel: FilterableDropdownSelectionModel) {
+ let selectionData = new Map()
+ items.forEach(i => {
+ if (i.document_count == this.list.selected.size) {
+ 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'})
- 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()
- })
- }
- }
- )
+ openTagsDropdown() {
+ this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
+ this.applySelectionData(s.selected_tags, this.tagSelectionModel)
})
}
- setCorrespondents(newCorrespondentsToggleableItems: ToggleableItem[]) {
- let changedCorrespondents: ChangedItems
- if (newCorrespondentsToggleableItems) {
- changedCorrespondents = this.checkForChangedItems(this.initialCorrespondentsToggleableItems, newCorrespondentsToggleableItems)
- if (changedCorrespondents.itemsToAdd.length == 0 && changedCorrespondents.itemsToRemove.length == 0) return
- }
+ openDocumentTypeDropdown() {
+ this.documentService.getSelectionData(Array.from(this.list.selected)).subscribe(s => {
+ this.applySelectionData(s.selected_document_types, this.documentTypeSelectionModel)
+ })
+ }
+
+ 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'})
modal.componentInstance.title = "Confirm Correspondent Assignment"
@@ -207,24 +118,21 @@ export class BulkEditorComponent {
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.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 => {
+ this.correspondentService.clearCache()
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
- }
+ setDocumentTypes(changedDocumentTypes: ChangedItems) {
+ 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"
@@ -234,12 +142,13 @@ export class BulkEditorComponent {
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.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 => {
+ this.documentService.clearCache()
modal.close()
}
)
@@ -250,7 +159,7 @@ export class BulkEditorComponent {
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.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)"