diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index 5b7d7ef13..504ec79ee 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -959,7 +959,7 @@ describe('DocumentDetailComponent', () => { component.reprocess() const modalCloseSpy = jest.spyOn(openModal, 'close') openModal.componentInstance.confirmClicked.next() - expect(reprocessSpy).toHaveBeenCalledWith([doc.id]) + expect(reprocessSpy).toHaveBeenCalledWith({ documents: [doc.id] }) expect(modalSpy).toHaveBeenCalled() expect(toastSpy).toHaveBeenCalled() expect(modalCloseSpy).toHaveBeenCalled() diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 25c854e53..91f448056 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -1379,25 +1379,27 @@ export class DocumentDetailComponent modal.componentInstance.btnCaption = $localize`Proceed` modal.componentInstance.confirmClicked.subscribe(() => { modal.componentInstance.buttonsEnabled = false - this.documentsService.reprocessDocuments([this.document.id]).subscribe({ - next: () => { - this.toastService.showInfo( - $localize`Reprocess operation for "${this.document.title}" will begin in the background.` - ) - if (modal) { - modal.close() - } - }, - error: (error) => { - if (modal) { - modal.componentInstance.buttonsEnabled = true - } - this.toastService.showError( - $localize`Error executing operation`, - error - ) - }, - }) + this.documentsService + .reprocessDocuments({ documents: [this.document.id] }) + .subscribe({ + next: () => { + this.toastService.showInfo( + $localize`Reprocess operation for "${this.document.title}" will begin in the background.` + ) + if (modal) { + modal.close() + } + }, + error: (error) => { + if (modal) { + modal.componentInstance.buttonsEnabled = true + } + this.toastService.showError( + $localize`Error executing operation`, + error + ) + }, + }) }) } diff --git a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts index da74da98a..1170a3b10 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/bulk-editor.component.spec.ts @@ -13,6 +13,7 @@ import { of, throwError } from 'rxjs' import { Correspondent } from 'src/app/data/correspondent' import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' import { DocumentType } from 'src/app/data/document-type' +import { FILTER_TITLE } from 'src/app/data/filter-rule-type' import { Results } from 'src/app/data/results' import { StoragePath } from 'src/app/data/storage-path' import { Tag } from 'src/app/data/tag' @@ -273,6 +274,24 @@ describe('BulkEditorComponent', () => { expect(component.customFieldsSelectionModel.selectionSize()).toEqual(1) }) + it('should apply list selection data to tags menu when all filtered documents are selected', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + fixture.detectChanges() + jest + .spyOn(documentListViewService, 'allSelected', 'get') + .mockReturnValue(true) + jest + .spyOn(documentListViewService, 'selectedCount', 'get') + .mockReturnValue(3) + documentListViewService.selectionData = selectionData + const getSelectionDataSpy = jest.spyOn(documentService, 'getSelectionData') + + component.openTagsDropdown() + + expect(getSelectionDataSpy).not.toHaveBeenCalled() + expect(component.tagSelectionModel.selectionSize()).toEqual(1) + }) + it('should execute modify tags bulk operation', () => { jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) jest @@ -307,6 +326,49 @@ describe('BulkEditorComponent', () => { ) // listAllFilteredIds }) + it('should execute modify tags bulk operation for all filtered documents', () => { + jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true) + jest + .spyOn(documentListViewService, 'documents', 'get') + .mockReturnValue([{ id: 3 }, { id: 4 }]) + jest + .spyOn(documentListViewService, 'selected', 'get') + .mockReturnValue(new Set([3, 4])) + jest + .spyOn(documentListViewService, 'allSelected', 'get') + .mockReturnValue(true) + jest + .spyOn(documentListViewService, 'filterRules', 'get') + .mockReturnValue([{ rule_type: FILTER_TITLE, value: 'apple' }]) + jest + .spyOn(documentListViewService, 'selectedCount', 'get') + .mockReturnValue(25) + jest + .spyOn(permissionsService, 'currentUserHasObjectPermissions') + .mockReturnValue(true) + component.showConfirmationDialogs = false + fixture.detectChanges() + + component.setTags({ + itemsToAdd: [{ id: 101 }], + itemsToRemove: [], + }) + + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/bulk_edit/` + ) + req.flush(true) + expect(req.request.body).toEqual({ + all: true, + filters: { title__icontains: 'apple' }, + method: 'modify_tags', + parameters: { add_tags: [101], remove_tags: [] }, + }) + httpTestingController.match( + `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` + ) // list reload + }) + it('should execute modify tags bulk operation with confirmation dialog if enabled', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[0])) @@ -1089,22 +1151,39 @@ describe('BulkEditorComponent', () => { component.downloadForm.get('downloadFileTypeArchive').patchValue(true) fixture.detectChanges() let downloadSpy = jest.spyOn(documentService, 'bulkDownload') + downloadSpy.mockReturnValue(of(new Blob())) //archive component.downloadSelected() - expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'archive', false) + expect(downloadSpy).toHaveBeenCalledWith( + { documents: [3, 4] }, + 'archive', + false + ) //originals component.downloadForm.get('downloadFileTypeArchive').patchValue(false) component.downloadForm.get('downloadFileTypeOriginals').patchValue(true) component.downloadSelected() - expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'originals', false) + expect(downloadSpy).toHaveBeenCalledWith( + { documents: [3, 4] }, + 'originals', + false + ) //both component.downloadForm.get('downloadFileTypeArchive').patchValue(true) component.downloadSelected() - expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', false) + expect(downloadSpy).toHaveBeenCalledWith( + { documents: [3, 4] }, + 'both', + false + ) //formatting component.downloadForm.get('downloadUseFormatting').patchValue(true) component.downloadSelected() - expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', true) + expect(downloadSpy).toHaveBeenCalledWith( + { documents: [3, 4] }, + 'both', + true + ) httpTestingController.match( `${environment.apiBaseUrl}documents/bulk_download/` @@ -1450,7 +1529,7 @@ describe('BulkEditorComponent', () => { expect(modal.componentInstance.customFields.length).toEqual(2) expect(modal.componentInstance.fieldsToAddIds).toEqual([1, 2]) - expect(modal.componentInstance.documents).toEqual([3, 4]) + expect(modal.componentInstance.documents).toEqual({ documents: [3, 4] }) modal.componentInstance.failed.emit() expect(toastServiceShowErrorSpy).toHaveBeenCalled() 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 b97cc76c4..0a3c182cc 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 @@ -31,6 +31,7 @@ import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service import { DocumentTypeService } from 'src/app/services/rest/document-type.service' import { DocumentBulkEditMethod, + DocumentSelectionQuery, DocumentService, MergeDocumentsRequest, } from 'src/app/services/rest/document.service' @@ -41,6 +42,7 @@ import { TagService } from 'src/app/services/rest/tag.service' import { SettingsService } from 'src/app/services/settings.service' import { ToastService } from 'src/app/services/toast.service' import { flattenTags } from 'src/app/utils/flatten-tags' +import { queryParamsFromFilterRules } from 'src/app/utils/query-params' import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-confirm-dialog/merge-confirm-dialog.component' import { RotateConfirmDialogComponent } from '../../common/confirm-dialog/rotate-confirm-dialog/rotate-confirm-dialog.component' import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component' @@ -261,17 +263,13 @@ export class BulkEditorComponent modal: NgbModalRef, method: DocumentBulkEditMethod, args: any, - overrideDocumentIDs?: number[] + overrideSelection?: DocumentSelectionQuery ) { if (modal) { modal.componentInstance.buttonsEnabled = false } this.documentService - .bulkEdit( - overrideDocumentIDs ?? Array.from(this.list.selected), - method, - args - ) + .bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args) .pipe(first()) .subscribe({ next: () => this.handleOperationSuccess(modal), @@ -329,7 +327,7 @@ export class BulkEditorComponent ) { let selectionData = new Map() items.forEach((i) => { - if (i.document_count == this.list.selected.size) { + if (i.document_count == this.list.selectedCount) { selectionData.set(i.id, ToggleableItemState.Selected) } else if (i.document_count > 0) { selectionData.set(i.id, ToggleableItemState.PartiallySelected) @@ -338,7 +336,31 @@ export class BulkEditorComponent selectionModel.init(selectionData) } + private getSelectionQuery(): DocumentSelectionQuery { + if (this.list.allSelected) { + return { + all: true, + filters: queryParamsFromFilterRules(this.list.filterRules), + } + } + + return { + documents: Array.from(this.list.selected), + } + } + + private getSelectionSize(): number { + return this.list.selectedCount + } + openTagsDropdown() { + if (this.list.allSelected) { + const selectionData = this.list.selectionData + this.tagDocumentCounts = selectionData?.selected_tags ?? [] + this.applySelectionData(this.tagDocumentCounts, this.tagSelectionModel) + return + } + this.documentService .getSelectionData(Array.from(this.list.selected)) .pipe(first()) @@ -349,6 +371,17 @@ export class BulkEditorComponent } openDocumentTypeDropdown() { + if (this.list.allSelected) { + const selectionData = this.list.selectionData + this.documentTypeDocumentCounts = + selectionData?.selected_document_types ?? [] + this.applySelectionData( + this.documentTypeDocumentCounts, + this.documentTypeSelectionModel + ) + return + } + this.documentService .getSelectionData(Array.from(this.list.selected)) .pipe(first()) @@ -362,6 +395,17 @@ export class BulkEditorComponent } openCorrespondentDropdown() { + if (this.list.allSelected) { + const selectionData = this.list.selectionData + this.correspondentDocumentCounts = + selectionData?.selected_correspondents ?? [] + this.applySelectionData( + this.correspondentDocumentCounts, + this.correspondentSelectionModel + ) + return + } + this.documentService .getSelectionData(Array.from(this.list.selected)) .pipe(first()) @@ -375,6 +419,17 @@ export class BulkEditorComponent } openStoragePathDropdown() { + if (this.list.allSelected) { + const selectionData = this.list.selectionData + this.storagePathDocumentCounts = + selectionData?.selected_storage_paths ?? [] + this.applySelectionData( + this.storagePathDocumentCounts, + this.storagePathsSelectionModel + ) + return + } + this.documentService .getSelectionData(Array.from(this.list.selected)) .pipe(first()) @@ -388,6 +443,17 @@ export class BulkEditorComponent } openCustomFieldsDropdown() { + if (this.list.allSelected) { + const selectionData = this.list.selectionData + this.customFieldDocumentCounts = + selectionData?.selected_custom_fields ?? [] + this.applySelectionData( + this.customFieldDocumentCounts, + this.customFieldsSelectionModel + ) + return + } + this.documentService .getSelectionData(Array.from(this.list.selected)) .pipe(first()) @@ -437,33 +503,33 @@ export class BulkEditorComponent changedTags.itemsToRemove.length == 0 ) { let tag = changedTags.itemsToAdd[0] - modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will add the tag "${tag.name}" to ${this.getSelectionSize()} selected document(s).` } else if ( changedTags.itemsToAdd.length > 1 && changedTags.itemsToRemove.length == 0 ) { modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList( changedTags.itemsToAdd - )} to ${this.list.selected.size} selected document(s).` + )} to ${this.getSelectionSize()} selected document(s).` } else if ( changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length == 1 ) { let tag = changedTags.itemsToRemove[0] - modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will remove the tag "${tag.name}" from ${this.getSelectionSize()} selected document(s).` } else if ( changedTags.itemsToAdd.length == 0 && changedTags.itemsToRemove.length > 1 ) { modal.componentInstance.message = $localize`This operation will remove the tags ${this._localizeList( changedTags.itemsToRemove - )} from ${this.list.selected.size} selected document(s).` + )} from ${this.getSelectionSize()} selected document(s).` } else { modal.componentInstance.message = $localize`This operation will add the tags ${this._localizeList( changedTags.itemsToAdd )} and remove the tags ${this._localizeList( changedTags.itemsToRemove - )} on ${this.list.selected.size} selected document(s).` + )} on ${this.getSelectionSize()} selected document(s).` } modal.componentInstance.btnClass = 'btn-warning' @@ -502,9 +568,9 @@ export class BulkEditorComponent }) modal.componentInstance.title = $localize`Confirm correspondent assignment` if (correspondent) { - modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will assign the correspondent "${correspondent.name}" to ${this.getSelectionSize()} selected document(s).` } else { - modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will remove the correspondent from ${this.getSelectionSize()} selected document(s).` } modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnCaption = $localize`Confirm` @@ -540,9 +606,9 @@ export class BulkEditorComponent }) modal.componentInstance.title = $localize`Confirm document type assignment` if (documentType) { - modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will assign the document type "${documentType.name}" to ${this.getSelectionSize()} selected document(s).` } else { - modal.componentInstance.message = $localize`This operation will remove the document type from ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will remove the document type from ${this.getSelectionSize()} selected document(s).` } modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnCaption = $localize`Confirm` @@ -578,9 +644,9 @@ export class BulkEditorComponent }) modal.componentInstance.title = $localize`Confirm storage path assignment` if (storagePath) { - modal.componentInstance.message = $localize`This operation will assign the storage path "${storagePath.name}" to ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will assign the storage path "${storagePath.name}" to ${this.getSelectionSize()} selected document(s).` } else { - modal.componentInstance.message = $localize`This operation will remove the storage path from ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will remove the storage path from ${this.getSelectionSize()} selected document(s).` } modal.componentInstance.btnClass = 'btn-warning' modal.componentInstance.btnCaption = $localize`Confirm` @@ -615,33 +681,33 @@ export class BulkEditorComponent changedCustomFields.itemsToRemove.length == 0 ) { let customField = changedCustomFields.itemsToAdd[0] - modal.componentInstance.message = $localize`This operation will assign the custom field "${customField.name}" to ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will assign the custom field "${customField.name}" to ${this.getSelectionSize()} selected document(s).` } else if ( changedCustomFields.itemsToAdd.length > 1 && changedCustomFields.itemsToRemove.length == 0 ) { modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList( changedCustomFields.itemsToAdd - )} to ${this.list.selected.size} selected document(s).` + )} to ${this.getSelectionSize()} selected document(s).` } else if ( changedCustomFields.itemsToAdd.length == 0 && changedCustomFields.itemsToRemove.length == 1 ) { let customField = changedCustomFields.itemsToRemove[0] - modal.componentInstance.message = $localize`This operation will remove the custom field "${customField.name}" from ${this.list.selected.size} selected document(s).` + modal.componentInstance.message = $localize`This operation will remove the custom field "${customField.name}" from ${this.getSelectionSize()} selected document(s).` } else if ( changedCustomFields.itemsToAdd.length == 0 && changedCustomFields.itemsToRemove.length > 1 ) { modal.componentInstance.message = $localize`This operation will remove the custom fields ${this._localizeList( changedCustomFields.itemsToRemove - )} from ${this.list.selected.size} selected document(s).` + )} from ${this.getSelectionSize()} selected document(s).` } else { modal.componentInstance.message = $localize`This operation will assign the custom fields ${this._localizeList( changedCustomFields.itemsToAdd )} and remove the custom fields ${this._localizeList( changedCustomFields.itemsToRemove - )} on ${this.list.selected.size} selected document(s).` + )} on ${this.getSelectionSize()} selected document(s).` } modal.componentInstance.btnClass = 'btn-warning' @@ -779,7 +845,7 @@ export class BulkEditorComponent backdrop: 'static', }) modal.componentInstance.title = $localize`Confirm` - modal.componentInstance.messageBold = $localize`Move ${this.list.selected.size} selected document(s) to the trash?` + modal.componentInstance.messageBold = $localize`Move ${this.getSelectionSize()} selected document(s) to the trash?` modal.componentInstance.message = $localize`Documents can be restored prior to permanent deletion.` modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnCaption = $localize`Move to trash` @@ -789,13 +855,13 @@ export class BulkEditorComponent modal.componentInstance.buttonsEnabled = false this.executeDocumentAction( modal, - this.documentService.deleteDocuments(Array.from(this.list.selected)) + this.documentService.deleteDocuments(this.getSelectionQuery()) ) }) } else { this.executeDocumentAction( null, - this.documentService.deleteDocuments(Array.from(this.list.selected)) + this.documentService.deleteDocuments(this.getSelectionQuery()) ) } } @@ -811,7 +877,7 @@ export class BulkEditorComponent : 'originals' this.documentService .bulkDownload( - Array.from(this.list.selected), + this.getSelectionQuery(), downloadFileType, this.downloadForm.get('downloadUseFormatting').value ) @@ -827,7 +893,7 @@ export class BulkEditorComponent backdrop: 'static', }) modal.componentInstance.title = $localize`Reprocess confirm` - modal.componentInstance.messageBold = $localize`This operation will permanently recreate the archive files for ${this.list.selected.size} selected document(s).` + modal.componentInstance.messageBold = $localize`This operation will permanently recreate the archive files for ${this.getSelectionSize()} selected document(s).` modal.componentInstance.message = $localize`The archive files will be re-generated with the current settings.` modal.componentInstance.btnClass = 'btn-danger' modal.componentInstance.btnCaption = $localize`Proceed` @@ -837,9 +903,7 @@ export class BulkEditorComponent modal.componentInstance.buttonsEnabled = false this.executeDocumentAction( modal, - this.documentService.reprocessDocuments( - Array.from(this.list.selected) - ) + this.documentService.reprocessDocuments(this.getSelectionQuery()) ) }) } @@ -866,7 +930,7 @@ export class BulkEditorComponent }) const rotateDialog = modal.componentInstance as RotateConfirmDialogComponent rotateDialog.title = $localize`Rotate confirm` - rotateDialog.messageBold = $localize`This operation will add rotated versions of the ${this.list.selected.size} document(s).` + rotateDialog.messageBold = $localize`This operation will add rotated versions of the ${this.getSelectionSize()} document(s).` rotateDialog.btnClass = 'btn-danger' rotateDialog.btnCaption = $localize`Proceed` rotateDialog.documentID = Array.from(this.list.selected)[0] @@ -877,7 +941,7 @@ export class BulkEditorComponent this.executeDocumentAction( modal, this.documentService.rotateDocuments( - Array.from(this.list.selected), + this.getSelectionQuery(), rotateDialog.degrees ) ) @@ -890,7 +954,7 @@ export class BulkEditorComponent }) const mergeDialog = modal.componentInstance as MergeConfirmDialogComponent mergeDialog.title = $localize`Merge confirm` - mergeDialog.messageBold = $localize`This operation will merge ${this.list.selected.size} selected documents into a new document.` + mergeDialog.messageBold = $localize`This operation will merge ${this.getSelectionSize()} selected documents into a new document.` mergeDialog.btnCaption = $localize`Proceed` mergeDialog.documentIDs = Array.from(this.list.selected) mergeDialog.confirmClicked @@ -935,7 +999,7 @@ export class BulkEditorComponent (item) => item.id ) - dialog.documents = Array.from(this.list.selected) + dialog.documents = this.getSelectionQuery() dialog.succeeded.subscribe((result) => { this.toastService.showInfo($localize`Custom fields updated.`) this.list.reload()