From d2197bf05e1caa9450272332b5252793e0d109d9 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sat, 14 Nov 2026 00:05:07 -0800 Subject: [PATCH] use the queued operation pathway across teh frontend --- .../bulk-editor/bulk-editor.component.spec.ts | 36 +++++---- .../bulk-editor/bulk-editor.component.ts | 69 +++-------------- .../services/rest/document.service.spec.ts | 9 +++ .../src/app/services/rest/document.service.ts | 76 ++++++++++++------- 4 files changed, 91 insertions(+), 99 deletions(-) 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 5296c7f0ae..d39705c416 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 @@ -1239,14 +1239,15 @@ describe('BulkEditorComponent', () => { let req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/delete/` ) - const notifySpy = jest - .spyOn(trashService, 'notifyMovedToTrash') + const trackSpy = jest + .spyOn(trashService, 'trackMovedToTrash') .mockImplementation(() => {}) - req.flush(true) + req.flush({ result: 'OK', task_id: 'delete-1' }) expect(req.request.body).toEqual({ documents: [3, 4], + from_webui: true, }) - expect(notifySpy).toHaveBeenCalledWith(expect.any(String), [3, 4]) + expect(trackSpy).toHaveBeenCalledWith('delete-1', 2, [3, 4]) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` ) // list reload @@ -1263,9 +1264,9 @@ describe('BulkEditorComponent', () => { req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/delete/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'delete-2' }) // ids aren't known when all matching documents are selected, so no undo - expect(notifySpy).toHaveBeenLastCalledWith(expect.any(String), null) + expect(trackSpy).toHaveBeenLastCalledWith('delete-2', 0, null) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` ) // list reload @@ -1345,10 +1346,11 @@ describe('BulkEditorComponent', () => { let req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/reprocess/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'reprocess-1' }) expect(req.request.body).toEqual({ documents: [3, 4], remote_ocr: false, + from_webui: true, }) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` @@ -1379,11 +1381,12 @@ describe('BulkEditorComponent', () => { let req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/rotate/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'rotate-1' }) expect(req.request.body).toEqual({ documents: [3, 4], degrees: 90, source_mode: 'latest_version', + from_webui: true, }) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` @@ -1421,10 +1424,11 @@ describe('BulkEditorComponent', () => { let req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/merge/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'merge-1' }) expect(req.request.body).toEqual({ documents: [3, 4], metadata_document_id: 3, + from_webui: true, }) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` @@ -1439,11 +1443,12 @@ describe('BulkEditorComponent', () => { req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/merge/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'merge-2' }) expect(req.request.body).toEqual({ documents: [3, 4], metadata_document_id: 3, delete_originals: true, + from_webui: true, }) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` @@ -1460,11 +1465,12 @@ describe('BulkEditorComponent', () => { req = httpTestingController.expectOne( `${environment.apiBaseUrl}documents/merge/` ) - req.flush(true) + req.flush({ result: 'OK', task_id: 'merge-3' }) expect(req.request.body).toEqual({ documents: [3, 4], metadata_document_id: 3, archive_fallback: true, + from_webui: true, }) httpTestingController.match( `${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&include_selection_data=true` @@ -1503,8 +1509,8 @@ describe('BulkEditorComponent', () => { .mockReturnValue(true) const mergeAsVersionsSpy = jest .spyOn(documentService, 'mergeDocumentsAsVersions') - .mockReturnValue(of(true)) - const toastSuccessSpy = jest.spyOn(toastService, 'showSuccess') + .mockReturnValue(of({ result: 'OK', task_id: 'versions-1' })) + const toastSpy = jest.spyOn(toastService, 'show') fixture.detectChanges() component.mergeSelectedAsVersions() @@ -1520,8 +1526,8 @@ describe('BulkEditorComponent', () => { `${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id` ) expect(documentListViewService.selected.size).toEqual(0) - expect(toastSuccessSpy).toHaveBeenCalledWith( - 'Documents merged as versions.' + expect(toastSpy).toHaveBeenCalledWith( + expect.objectContaining({ content: 'Bulk operation queued.' }) ) }) 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 066f941bff..705424429e 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,8 @@ 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 { BulkOperationResponse } from 'src/app/data/bulk-operation' 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' @@ -37,7 +30,6 @@ import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { Workflow } from 'src/app/data/workflow' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { DocumentListViewService } from 'src/app/services/document-list-view.service' -import { OpenDocumentsService } from 'src/app/services/open-documents.service' import { PermissionAction, PermissionsService, @@ -52,7 +44,6 @@ import { DocumentService, MergeDocumentsRequest, } from 'src/app/services/rest/document.service' -import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { ShareLinkBundleService } from 'src/app/services/rest/share-link-bundle.service' import { WorkflowService } from 'src/app/services/rest/workflow.service' import { StoragePathService } from 'src/app/services/rest/storage-path.service' @@ -110,13 +101,11 @@ export class BulkEditorComponent private documentService = inject(DocumentService) private trashService = inject(TrashService) private modalService = inject(NgbModal) - private openDocumentService = inject(OpenDocumentsService) private settings = inject(SettingsService) private toastService = inject(ToastService) private storagePathService = inject(StoragePathService) private customFieldService = inject(CustomFieldsService) public readonly permissionService = inject(PermissionsService) - private savedViewService = inject(SavedViewService) private readonly shareLinkBundleService = inject(ShareLinkBundleService) private readonly workflowService = inject(WorkflowService) private readonly router = inject(Router) @@ -371,41 +360,21 @@ export class BulkEditorComponent private executeDocumentAction( modal: NgbModalRef, - request: Observable, - options: { clearSelection?: boolean; successMessage?: string } = {} + request: Observable, + onQueued?: (taskId: string) => void ) { if (modal) { modal.componentInstance.buttonsEnabled.set(false) } request.pipe(first()).subscribe({ - next: () => { - this.handleOperationSuccess(modal, options.clearSelection ?? false) - if (options.successMessage) { - this.toastService.showSuccess(options.successMessage) - } + next: ({ task_id }) => { + onQueued?.(task_id) + this.handleOperationQueued(modal, task_id) }, error: (error) => this.handleOperationError(modal, error), }) } - private handleOperationSuccess( - modal: NgbModalRef, - clearSelection: boolean = false - ) { - if (clearSelection) { - this.list.selected.clear() - } - this.list.reload() - this.list.reduceSelectionToFilter() - this.list.selected.forEach((id) => { - this.openDocumentService.refreshDocument(id) - }) - this.savedViewService.maybeRefreshDocumentCounts() - if (modal) { - modal.close() - } - } - private handleOperationError(modal: NgbModalRef, error: any) { if (modal) { modal.componentInstance.buttonsEnabled.set(true) @@ -973,16 +942,8 @@ export class BulkEditorComponent const count = this.getSelectionSize() this.executeDocumentAction( modal, - this.documentService - .deleteDocuments(this.getSelectionQuery()) - .pipe( - tap(() => - this.trashService.notifyMovedToTrash( - $localize`Moved ${count} document(s) to the trash`, - documents - ) - ) - ) + this.documentService.deleteDocuments(this.getSelectionQuery()), + (taskId) => this.trashService.trackMovedToTrash(taskId, count, documents) ) } @@ -1096,11 +1057,7 @@ export class BulkEditorComponent mergeDialog.buttonsEnabled.set(false) this.executeDocumentAction( modal, - this.documentService.mergeDocuments(mergeDialog.documentIDs(), args), - { clearSelection: !!args.delete_originals } - ) - this.toastService.showInfo( - $localize`Merged document will be queued for consumption.` + this.documentService.mergeDocuments(mergeDialog.documentIDs(), args) ) }) } @@ -1125,11 +1082,7 @@ export class BulkEditorComponent this.documentService.mergeDocumentsAsVersions( mergeDialog.documentIDs(), mergeDialog.rootDocumentID() - ), - { - clearSelection: true, - successMessage: $localize`Documents merged as versions.`, - } + ) ) }) } 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 eaa5d60f49..17cfbc9066 100644 --- a/src-ui/src/app/services/rest/document.service.spec.ts +++ b/src-ui/src/app/services/rest/document.service.spec.ts @@ -290,6 +290,7 @@ describe(`DocumentService`, () => { expect(req.request.method).toEqual('POST') expect(req.request.body).toEqual({ documents: ids, + from_webui: true, }) }) @@ -303,6 +304,7 @@ describe(`DocumentService`, () => { expect(req.request.body).toEqual({ documents: ids, remote_ocr: false, + from_webui: true, }) }) @@ -317,6 +319,7 @@ describe(`DocumentService`, () => { expect(req.request.body).toEqual({ documents: ids, remote_ocr: true, + from_webui: true, }) }) @@ -331,6 +334,7 @@ describe(`DocumentService`, () => { documents: ids, degrees: 90, source_mode: 'latest_version', + from_webui: true, }) }) @@ -346,6 +350,7 @@ describe(`DocumentService`, () => { documents: ids, metadata_document_id: 1, delete_originals: true, + from_webui: true, }) }) @@ -359,6 +364,7 @@ describe(`DocumentService`, () => { expect(req.request.body).toEqual({ documents: ids, root_document_id: 2, + from_webui: true, }) }) @@ -374,6 +380,7 @@ describe(`DocumentService`, () => { documents: ids, root_document_id: 2, version_label: 'Imported', + from_webui: true, }) }) @@ -388,6 +395,7 @@ describe(`DocumentService`, () => { expect(req.request.body).toEqual({ documents: ids, operations: [{ page: 1, rotate: 90, doc: 0 }], + from_webui: true, }) }) @@ -403,6 +411,7 @@ describe(`DocumentService`, () => { documents: ids, password: 'secret', update_document: true, + from_webui: true, }) }) diff --git a/src-ui/src/app/services/rest/document.service.ts b/src-ui/src/app/services/rest/document.service.ts index d0822e8b00..01faf53c6b 100644 --- a/src-ui/src/app/services/rest/document.service.ts +++ b/src-ui/src/app/services/rest/document.service.ts @@ -360,10 +360,14 @@ export class DocumentService extends AbstractPaperlessService { selection: DocumentSelectionQuery, remoteOcr: boolean = false ) { - return this.http.post(this.getResourceUrl(null, 'reprocess'), { - ...selection, - remote_ocr: remoteOcr, - }) + return this.http.post( + this.getResourceUrl(null, 'reprocess'), + { + ...selection, + remote_ocr: remoteOcr, + from_webui: true, + } + ) } rotateDocuments( @@ -371,18 +375,26 @@ export class DocumentService extends AbstractPaperlessService { degrees: number, sourceMode: BulkEditSourceMode = BulkEditSourceMode.LATEST_VERSION ) { - return this.http.post(this.getResourceUrl(null, 'rotate'), { - ...selection, - degrees, - source_mode: sourceMode, - }) + return this.http.post( + this.getResourceUrl(null, 'rotate'), + { + ...selection, + degrees, + source_mode: sourceMode, + from_webui: true, + } + ) } mergeDocuments(ids: number[], request: MergeDocumentsRequest = {}) { - return this.http.post(this.getResourceUrl(null, 'merge'), { - documents: ids, - ...request, - }) + return this.http.post( + this.getResourceUrl(null, 'merge'), + { + documents: ids, + ...request, + from_webui: true, + } + ) } mergeDocumentsAsVersions( @@ -390,28 +402,40 @@ export class DocumentService extends AbstractPaperlessService { rootDocumentId: number, versionLabel?: string ) { - return this.http.post(this.getResourceUrl(null, 'merge_as_versions'), { - documents: ids, - root_document_id: rootDocumentId, - ...(versionLabel ? { version_label: versionLabel } : {}), - }) + return this.http.post( + this.getResourceUrl(null, 'merge_as_versions'), + { + documents: ids, + root_document_id: rootDocumentId, + ...(versionLabel ? { version_label: versionLabel } : {}), + from_webui: true, + } + ) } editPdfDocuments(ids: number[], request: EditPdfDocumentsRequest) { - return this.http.post(this.getResourceUrl(null, 'edit_pdf'), { - documents: ids, - ...request, - }) + return this.http.post( + this.getResourceUrl(null, 'edit_pdf'), + { + documents: ids, + ...request, + from_webui: true, + } + ) } removePasswordDocuments( ids: number[], request: RemovePasswordDocumentsRequest ) { - return this.http.post(this.getResourceUrl(null, 'remove_password'), { - documents: ids, - ...request, - }) + return this.http.post( + this.getResourceUrl(null, 'remove_password'), + { + documents: ids, + ...request, + from_webui: true, + } + ) } getSelectionData(