From e06e1ad3b053ea06330d8df285081a950a9913e1 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 3 Nov 2026 09:28:33 -0800 Subject: [PATCH] Update frontend bulk operatoins to pass from_webui, get the task id --- .../bulk-editor/bulk-editor.component.spec.ts | 30 ++++++++++ .../bulk-editor/bulk-editor.component.ts | 56 ++++++++++--------- ...ustom-fields-bulk-edit-dialog.component.ts | 7 ++- src-ui/src/app/data/bulk-operation.ts | 4 ++ .../services/rest/document.service.spec.ts | 3 + .../src/app/services/rest/document.service.ts | 25 ++++++--- 6 files changed, 90 insertions(+), 35 deletions(-) create mode 100644 src-ui/src/app/data/bulk-operation.ts 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 97be61b070..7549ea1323 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 @@ -561,6 +561,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'modify_tags', parameters: { add_tags: [101], remove_tags: [] }, @@ -609,6 +610,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, all: true, filters: { title_search: 'apple' }, excluded_documents: [4], @@ -731,6 +733,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'set_correspondent', parameters: { correspondent: 101 }, @@ -829,6 +832,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'set_document_type', parameters: { document_type: 101 }, @@ -927,6 +931,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'set_storage_path', parameters: { storage_path: 101 }, @@ -1025,6 +1030,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'modify_custom_fields', parameters: { add_custom_fields: [101], remove_custom_fields: [102] }, @@ -1171,6 +1177,7 @@ describe('BulkEditorComponent', () => { .mockImplementation(() => {}) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], }) expect(notifySpy).toHaveBeenCalledWith(expect.any(String), [3, 4]) @@ -1540,6 +1547,28 @@ describe('BulkEditorComponent', () => { ) }) + it('closes a queued permissions dialog without refreshing before completion', () => { + let modal: NgbModalRef + modalService.activeInstances.subscribe((instances) => { + if (instances.length) modal = instances[0] + }) + const reload = jest.spyOn(documentListViewService, 'reload') + component.setPermissions() + const close = jest.spyOn(modal, 'close') + modal.componentInstance.confirmClicked.emit({ + permissions: {}, + merge: true, + }) + const req = httpTestingController.expectOne( + `${environment.apiBaseUrl}documents/bulk_edit/` + ) + req.flush({ result: 'OK', task_id: 'queued-operation' }) + expect(close).toHaveBeenCalledTimes(1) + expect(reload).not.toHaveBeenCalled() + expect(documentListViewService.selected.size).toBe(0) + expect(documentListViewService.allSelected).toBe(false) + }) + it('should support bulk permissions update', () => { let modal: NgbModalRef modalService.activeInstances.subscribe((m) => (modal = m[0])) @@ -1574,6 +1603,7 @@ describe('BulkEditorComponent', () => { ) req.flush(true) expect(req.request.body).toEqual({ + from_webui: true, documents: [3, 4], method: 'set_permissions', parameters: { 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 067138b3a2..dda5249754 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 @@ -19,15 +19,7 @@ import { NgbModalRef, } from '@ng-bootstrap/ng-bootstrap' import { saveAs } from 'file-saver' -import { - first, - map, - Observable, - Subject, - switchMap, - takeUntil, - tap, -} from 'rxjs' +import { first, map, Observable, Subject, switchMap, takeUntil } from 'rxjs' import { BootstrapIconComponent } from 'src/app/components/common/bootstrap-icon/bootstrap-icon.component' import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' import { CustomField } from 'src/app/data/custom-field' @@ -299,7 +291,14 @@ export class BulkEditorComponent .bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args) .pipe(first()) .subscribe({ - next: () => this.handleOperationSuccess(modal), + next: (response) => { + if (response.task_id) { + modal?.close() + this.list.selectNone() + } else { + this.handleOperationSuccess(modal) + } + }, error: (error) => this.handleOperationError(modal, error), }) } @@ -906,19 +905,26 @@ export class BulkEditorComponent ? null : Array.from(this.list.selected) const count = this.getSelectionSize() - this.executeDocumentAction( - modal, - this.documentService - .deleteDocuments(this.getSelectionQuery()) - .pipe( - tap(() => + this.documentService + .deleteDocuments(this.getSelectionQuery()) + .pipe(first()) + .subscribe({ + next: (response) => { + const completed = () => this.trashService.notifyMovedToTrash( $localize`Moved ${count} document(s) to the trash`, documents ) - ) - ) - ) + if (response.task_id) { + modal?.close() + this.list.selectNone() + } else { + this.handleOperationSuccess(modal) + completed() + } + }, + error: (error) => this.handleOperationError(modal, error), + }) } downloadSelected() { @@ -1089,12 +1095,12 @@ export class BulkEditorComponent dialog.selection = this.getSelectionQuery() dialog.selectionCount = this.getSelectionSize() dialog.succeeded.subscribe((result) => { - this.toastService.showSuccess($localize`Custom fields updated.`) - this.list.reload() - this.list.reduceSelectionToFilter() - this.list.selected.forEach((id) => { - this.openDocumentService.refreshDocument(id) - }) + if (result?.task_id) { + this.list.selectNone() + } else { + this.toastService.showSuccess($localize`Custom fields updated.`) + this.handleOperationSuccess(null) + } }) dialog.failed.subscribe((error) => { this.toastService.showError( diff --git a/src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.ts b/src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.ts index e4eda79c89..920d8cadca 100644 --- a/src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.ts +++ b/src-ui/src/app/components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component.ts @@ -103,6 +103,8 @@ export class CustomFieldsBulkEditDialogComponent { } public save() { + if (this.networkActive) return + this.networkActive = true this.documentService .bulkEdit(this.selection, 'modify_custom_fields', { add_custom_fields: this.form.value, @@ -110,11 +112,12 @@ export class CustomFieldsBulkEditDialogComponent { }) .pipe(first()) .subscribe({ - next: () => { + next: (response) => { this.activeModal.close() - this.succeeded.emit() + this.succeeded.emit(response) }, error: (error) => { + this.networkActive = false this.failed.emit(error) }, }) diff --git a/src-ui/src/app/data/bulk-operation.ts b/src-ui/src/app/data/bulk-operation.ts new file mode 100644 index 0000000000..0728a12e5b --- /dev/null +++ b/src-ui/src/app/data/bulk-operation.ts @@ -0,0 +1,4 @@ +export interface BulkOperationResponse { + result: string + task_id?: string +} diff --git a/src-ui/src/app/services/rest/document.service.spec.ts b/src-ui/src/app/services/rest/document.service.spec.ts index 4250d8e84f..ec6ac248ff 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -248,6 +248,7 @@ describe(`DocumentService`, () => { ) expect(req.request.method).toEqual('POST') expect(req.request.body).toEqual({ + from_webui: true, documents: ids, method, parameters, @@ -271,6 +272,7 @@ describe(`DocumentService`, () => { ) expect(req.request.method).toEqual('POST') expect(req.request.body).toEqual({ + from_webui: true, all: true, filters: { title__icontains: 'apple' }, excluded_documents: [2, 3], @@ -287,6 +289,7 @@ describe(`DocumentService`, () => { ) expect(req.request.method).toEqual('POST') expect(req.request.body).toEqual({ + from_webui: true, documents: ids, }) }) diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index 3272d9695e..a09109bfbe 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -14,6 +14,7 @@ import { DocumentSuggestions } from 'src/app/data/document-suggestions' import { FilterRule } from 'src/app/data/filter-rule' import { Results, SelectionData } from 'src/app/data/results' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' +import { BulkOperationResponse } from '../../data/bulk-operation' import { queryParamsFromFilterRules } from '../../utils/query-params' import { PermissionAction, @@ -337,17 +338,25 @@ export class DocumentService extends AbstractPaperlessService { method: DocumentBulkEditMethod, args: any ) { - return this.http.post(this.getResourceUrl(null, 'bulk_edit'), { - ...selection, - method: method, - parameters: args, - }) + return this.http.post( + this.getResourceUrl(null, 'bulk_edit'), + { + ...selection, + method: method, + from_webui: true, + parameters: args, + } + ) } deleteDocuments(selection: DocumentSelectionQuery) { - return this.http.post(this.getResourceUrl(null, 'delete'), { - ...selection, - }) + return this.http.post( + this.getResourceUrl(null, 'delete'), + { + ...selection, + from_webui: true, + } + ) } reprocessDocuments(